diff --git a/.babelrc b/.babelrc index e0c2a5a..c2efdec 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { - presets: ['es2015'] + presets: ['es2015'], + only: 'scripts/**.es' } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..481327c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true +[*] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 877c75e..19c7031 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ npm-debug.log node_modules/ assets/ -_repos/ -_output/ -_tmp/ +gitlogg.json +scripts/**/*.js +scripts/**/*.js.map diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..862ce51 --- /dev/null +++ b/.npmignore @@ -0,0 +1,8 @@ +*.DS_Store +npm-debug.log + +node_modules/ +assets/ +gitlogg.json +.editorconfig +.babelrc \ No newline at end of file diff --git a/package.json b/package.json index 39f8093..b2141bb 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,23 @@ }, "copyright": "Copyright (c) Wallace Sidhrée - All rights reserved.", "license": "MIT", - "devDependencies": { - "babel-preset-es2015": "^6.9.0", - "chalk": "^1.1.3" + "bin": { + "gitlogg": "scripts/gitlogg.sh" }, "scripts": { - "setup": "npm install babel-cli -g && npm install && mkdir -p _output && mkdir -p _repos && mkdir -p _tmp", - "gitlogg": "./scripts/gitlogg.sh" + "build": "babel --source-maps --out-dir scripts scripts", + "test": "GITLOGG_DEV=1 ./gitlogg/gitlogg.sh ./", + "prepublish": "npm run build" + }, + "dependencies": { + "chalk": "^1.1.3", + "lodash": "^4.17.2", + "which": "^1.2.12" + }, + "devDependencies": { + "babel-cli": "^6.18.0", + "babel-preset-es2015": "^6.9.0", + "source-map-support": "^0.4.6" }, "engines": { "node": ">=4.3.0" diff --git a/scripts/colors.sh b/scripts/colors.sh old mode 100755 new mode 100644 index cf1a6ba..3b219a5 --- a/scripts/colors.sh +++ b/scripts/colors.sh @@ -1,5 +1,3 @@ -#!/usr/bin/bash - # Extracted command line colors for nicer output # Text Reset diff --git a/scripts/git-log-fields.es b/scripts/git-log-fields.es new file mode 100644 index 0000000..a18b572 --- /dev/null +++ b/scripts/git-log-fields.es @@ -0,0 +1,231 @@ + +const fields = exports.fields = [ + { code: '%H', + name: 'commit hash', + fullDescription: 'commit hash', + description: '', + identifier: 'commit_hash' }, + { code: '%h', + name: 'abbreviated commit hash', + fullDescription: 'abbreviated commit hash', + description: '', + identifier: 'abbreviated_commit_hash' }, + { code: '%T', + name: 'tree hash', + fullDescription: 'tree hash', + description: '', + identifier: 'tree_hash' }, + { code: '%t', + name: 'abbreviated tree hash', + fullDescription: 'abbreviated tree hash', + description: '', + identifier: 'abbreviated_tree_hash' }, + { code: '%P', + name: 'parent hashes', + fullDescription: 'parent hashes', + description: '', + identifier: 'parent_hashes' }, + { code: '%p', + name: 'abbreviated parent hashes', + fullDescription: 'abbreviated parent hashes', + description: '', + identifier: 'abbreviated_parent_hashes' }, + { code: '%an', + name: 'author name', + fullDescription: 'author name', + description: '', + identifier: 'author_name' }, + { code: '%aN', + name: 'author name', + fullDescription: 'author name (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + description: ' (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + identifier: 'author_name_mailmap' }, + { code: '%ae', + name: 'author email', + fullDescription: 'author email', + description: '', + identifier: 'author_email' }, + { code: '%aE', + name: 'author email', + fullDescription: 'author email (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + description: ' (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + identifier: 'author_email_mailmap' }, + // { code: '%ad', + // name: 'author date', + // fullDescription: 'author date (format respects --date= option)', + // description: ' (format respects --date= option)', + // identifier: 'author_date_(format_respects_--date=_option)' }, + // { code: '%aD', + // name: 'author date', + // fullDescription: 'author date, RFC2822 style', + // description: ', RFC2822 style', + // identifier: 'author_date_RFC2822_style' }, + { code: '%ar', + name: 'author date', + fullDescription: 'author date, relative', + description: ', relative', + identifier: 'author_date_relative' }, + // { code: '%at', + // name: 'author date', + // fullDescription: 'author date, UNIX timestamp', + // description: ', UNIX timestamp', + // identifier: 'author_date_UNIX_timestamp' }, + // { code: '%ai', + // name: 'author date', + // fullDescription: 'author date, ISO 8601-like format', + // description: ', ISO 8601-like format', + // identifier: 'author_date_ISO_8601-like_format' }, + { code: '%aI', + name: 'author date', + fullDescription: 'author date, strict ISO 8601 format', + description: ', strict ISO 8601 format', + identifier: 'author_date' }, + { code: '%cn', + name: 'committer name', + fullDescription: 'committer name', + description: '', + identifier: 'committer_name' }, + { code: '%cN', + name: 'committer name', + fullDescription: 'committer name (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + description: ' (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + identifier: 'committer_name_mailmap' }, + { code: '%ce', + name: 'committer email', + fullDescription: 'committer email', + description: '', + identifier: 'committer_email' }, + { code: '%cE', + name: 'committer email', + fullDescription: 'committer email (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + description: ' (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + identifier: 'committer_email_mailmap' }, + // { code: '%cd', + // name: 'committer date', + // fullDescription: 'committer date (format respects --date= option)', + // description: ' (format respects --date= option)', + // identifier: 'committer_date_(format_respects_--date=_option)' }, + // { code: '%cD', + // name: 'committer date', + // fullDescription: 'committer date, RFC2822 style', + // description: ', RFC2822 style', + // identifier: 'committer_date_RFC2822_style' }, + { code: '%cr', + name: 'committer date', + fullDescription: 'committer date, relative', + description: ', relative', + identifier: 'committer_date_relative' }, + // { code: '%ct', + // name: 'committer date', + // fullDescription: 'committer date, UNIX timestamp', + // description: ', UNIX timestamp', + // identifier: 'committer_date_UNIX_timestamp' }, + // { code: '%ci', + // name: 'committer date', + // fullDescription: 'committer date, ISO 8601-like format', + // description: ', ISO 8601-like format', + // identifier: 'committer_date_ISO_8601-like_format' }, + { code: '%cI', + name: 'committer date', + fullDescription: 'committer date, strict ISO 8601 format', + description: ', strict ISO 8601 format', + identifier: 'committer_date' }, + // { code: '%d', + // name: 'ref names', + // fullDescription: 'ref names, like the --decorate option of git-log[1]', + // description: ', like the --decorate option of git-log[1]', + // identifier: 'ref_names' }, + { code: '%D', + name: 'ref names without the "', + fullDescription: 'ref names without the " (", ")" wrapping.', + description: ' (", ")" wrapping.', + identifier: 'ref_names' }, + { code: '%e', + name: 'encoding', + fullDescription: 'encoding', + description: '', + identifier: 'encoding' }, + { code: '%s', + name: 'subject', + fullDescription: 'subject', + description: '', + identifier: 'subject' }, + { code: '%f', + name: 'sanitized subject line', + fullDescription: 'sanitized subject line, suitable for a filename', + description: ', suitable for a filename', + identifier: 'sanitized_subject' }, + { code: '%b', + name: 'body', + fullDescription: 'body', + description: '', + identifier: 'body' }, + { code: '%B', + name: 'raw body', + fullDescription: 'raw body (unwrapped subject and body)', + description: ' (unwrapped subject and body)', + identifier: 'raw_body' }, + { code: '%N', + name: 'commit notes', + fullDescription: 'commit notes', + description: '', + identifier: 'commit_notes' }, + { code: '%GG', + name: 'raw verification message from GPG for a signed commit', + fullDescription: 'raw verification message from GPG for a signed commit', + description: '', + identifier: 'raw_GPG_verification_message' }, + { code: '%G?', + name: 'show "G" for a good', + fullDescription: 'show "G" for a good (valid) signature, "B" for a bad signature, "U" for a good signature with unknown validity, "X" for a good signature that has expired, "Y" for a good signature made by an expired key, "R" for a good signature made by a revoked key, "E" if the signature cannot be checked (e.g. missing key) and "N" for no signature', + description: ' (valid) signature, "B" for a bad signature, "U" for a good signature with unknown validity, "X" for a good signature that has expired, "Y" for a good signature made by an expired key, "R" for a good signature made by a revoked key, "E" if the signature cannot be checked (e.g. missing key) and "N" for no signature', + identifier: 'signature_validity' }, + { code: '%GS', + name: 'show the name of the signer for a signed commit', + fullDescription: 'show the name of the signer for a signed commit', + description: '', + identifier: 'signer_name' }, + { code: '%GK', + name: 'show the key used to sign a signed commit', + fullDescription: 'show the key used to sign a signed commit', + description: '', + identifier: 'key' }, + { code: '%gD', + name: 'reflog selector', + fullDescription: 'reflog selector, e.g., refs/stash@{1} or refs/stash@{2 minutes ago}; the format follows the rules described for the -g option. The portion before the @ is the refname as given on the command line (so git log -g refs/heads/master would yield refs/heads/master@{0}).', + description: ', e.g., refs/stash@{1} or refs/stash@{2 minutes ago}; the format follows the rules described for the -g option. The portion before the @ is the refname as given on the command line (so git log -g refs/heads/master would yield refs/heads/master@{0}).', + identifier: 'reflog_selector' }, + { code: '%gd', + name: 'shortened reflog selector', + fullDescription: 'shortened reflog selector; same as %gD, but the refname portion is shortened for human readability (so refs/heads/master becomes just master).', + description: '; same as %gD, but the refname portion is shortened for human readability (so refs/heads/master becomes just master).', + identifier: 'shortened_reflog_selector' }, + { code: '%gn', + name: 'reflog identity name', + fullDescription: 'reflog identity name', + description: '', + identifier: 'reflog_identity_name' }, + { code: '%gN', + name: 'reflog identity name', + fullDescription: 'reflog identity name (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + description: ' (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + identifier: 'reflog_identity_name_mailmap' }, + { code: '%ge', + name: 'reflog identity email', + fullDescription: 'reflog identity email', + description: '', + identifier: 'reflog_identity_email' }, + { code: '%gE', + name: 'reflog identity email', + fullDescription: 'reflog identity email (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + description: ' (respecting .mailmap, see git-shortlog[1] or git-blame[1])', + identifier: 'reflog_identity_email_mailmap' }, + { code: '%gs', + name: 'reflog subject', + fullDescription: 'reflog subject', + description: '', + identifier: 'reflog_subject' } ]; + +const formatString = exports.formatString = fields.map(field => + `${ field.code }%x00` +).join(''); diff --git a/scripts/git-log-fields.generate.es b/scripts/git-log-fields.generate.es new file mode 100644 index 0000000..4ddcc32 --- /dev/null +++ b/scripts/git-log-fields.generate.es @@ -0,0 +1,156 @@ +// Code generator: generates JS list of all git log fields + +const {groupBy, each} = require('lodash'); +const {inspect} = require('util'); +const fs = require('fs'); +const path = require('path'); + +// Copy-pasted from https://git-scm.com/docs/pretty-formats, with all formatting-related codes removed. +const gitLogFieldDocs = ` +'%H': commit hash + +'%h': abbreviated commit hash + +'%T': tree hash + +'%t': abbreviated tree hash + +'%P': parent hashes + +'%p': abbreviated parent hashes + +'%an': author name + +'%aN': author name (respecting .mailmap, see git-shortlog[1] or git-blame[1]) + +'%ae': author email + +'%aE': author email (respecting .mailmap, see git-shortlog[1] or git-blame[1]) + +'%ad': author date (format respects --date= option) + +'%aD': author date, RFC2822 style + +'%ar': author date, relative + +'%at': author date, UNIX timestamp + +'%ai': author date, ISO 8601-like format + +'%aI': author date, strict ISO 8601 format + +'%cn': committer name + +'%cN': committer name (respecting .mailmap, see git-shortlog[1] or git-blame[1]) + +'%ce': committer email + +'%cE': committer email (respecting .mailmap, see git-shortlog[1] or git-blame[1]) + +'%cd': committer date (format respects --date= option) + +'%cD': committer date, RFC2822 style + +'%cr': committer date, relative + +'%ct': committer date, UNIX timestamp + +'%ci': committer date, ISO 8601-like format + +'%cI': committer date, strict ISO 8601 format + +'%d': ref names, like the --decorate option of git-log[1] + +'%D': ref names without the " (", ")" wrapping. + +'%e': encoding + +'%s': subject + +'%f': sanitized subject line, suitable for a filename + +'%b': body + +'%B': raw body (unwrapped subject and body) + +'%N': commit notes + +'%GG': raw verification message from GPG for a signed commit + +'%G?': show "G" for a good (valid) signature, "B" for a bad signature, "U" for a good signature with unknown validity, "X" for a good signature that has expired, "Y" for a good signature made by an expired key, "R" for a good signature made by a revoked key, "E" if the signature cannot be checked (e.g. missing key) and "N" for no signature + +'%GS': show the name of the signer for a signed commit + +'%GK': show the key used to sign a signed commit + +'%gD': reflog selector, e.g., refs/stash@{1} or refs/stash@{2 minutes ago}; the format follows the rules described for the -g option. The portion before the @ is the refname as given on the command line (so git log -g refs/heads/master would yield refs/heads/master@{0}). + +'%gd': shortened reflog selector; same as %gD, but the refname portion is shortened for human readability (so refs/heads/master becomes just master). + +'%gn': reflog identity name + +'%gN': reflog identity name (respecting .mailmap, see git-shortlog[1] or git-blame[1]) + +'%ge': reflog identity email + +'%gE': reflog identity email (respecting .mailmap, see git-shortlog[1] or git-blame[1]) + +'%gs': reflog subject + +// '%Cred': switch color to red +// +// '%Cgreen': switch color to green +// +// '%Cblue': switch color to blue +// +// '%Creset': reset color +// +// '%C(…​)': color specification, as described under Values in the "CONFIGURATION FILE" section of git-config[1]; adding auto, at the beginning will emit color only when colors are enabled for log output (by color.diff, color.ui, or --color, and respecting the auto settings of the former if we are going to a terminal). auto alone (i.e. %C(auto)) will turn on auto coloring on the next placeholders until the color is switched again. +// +// '%m': left (<), right (>) or boundary (-) mark +// +// '%n': newline +// +// '%%': a raw '%' +// +// '%x00': print a byte from a hex code +// +// '%w([[,[,]]])': switch line wrapping, like the -w option of git-shortlog[1]. +// +// '%<([,trunc|ltrunc|mtrunc])': make the next placeholder take at least N columns, padding spaces on the right if necessary. Optionally truncate at the beginning (ltrunc), the middle (mtrunc) or the end (trunc) if the output is longer than N columns. Note that truncating only works correctly with N >= 2. +// +// '%<|()': make the next placeholder take at least until Nth columns, padding spaces on the right if necessary +// +// '%>()', '%>|()': similar to '%<()', '%<|()' respectively, but padding spaces on the left +// +// '%>>()', '%>>|()': similar to '%>()', '%>|()' respectively, except that if the next placeholder takes more spaces than given and there are spaces on its left, use those spaces +// +// '%><()', '%><|()': similar to '% <()', '%<|()' respectively, but padding both sides (i.e. the text is centered) +`; + +const fields = gitLogFieldDocs.split(/\r\n|\n/).filter(line => ( + line && line.indexOf('//') != 0 +)).map(line => { + const [, code, fullDescription, name, description] = line.match(/'(.*?)': ((.*?)((?:,|;| \(|$).*))/); + return { + code, + name, + fullDescription, + description, + identifier: name.replace(/ /g, '_') + }; +}); + +// For fields that end up with identical identifiers, give them longer, unique identifiers +each(groupBy(fields, 'identifier'), group => { + if(group.length < 2) return; + group.forEach(field => { + field.identifier = field.fullDescription.replace(/[,;]? /g, '_'); + }); +}); + +const output = ` +exports.fields = ${ inspect(fields) }; +`; + +fs.writeFileSync(path.join(__dirname, 'git-log-fields.es'), output); \ No newline at end of file diff --git a/scripts/gitlogg-generate-log.sh b/scripts/gitlogg-generate-log.sh old mode 100755 new mode 100644 index 5ae192f..86ca965 --- a/scripts/gitlogg-generate-log.sh +++ b/scripts/gitlogg-generate-log.sh @@ -1,40 +1,32 @@ -#!/bin/bash - -my_dir="$(dirname "$0")" -cd $my_dir - -source "colors.sh" - -cd .. - -# define the absolute path to the directory that contains all your repositories. -yourpath='./_repos/' - -# define temporary 'git log' output file that will be parsed to 'json' -tempOutputFile='_tmp/gitlogg.tmp' - -# ensure file exists -mkdir -p ${tempOutputFile%%.*} -touch $tempOutputFile - -# name and path to this very script, for output message purposes -thisFile='./scripts/gitlogg-generate-log.sh' +source "$__dirname/colors.sh" + +runJs() { + local script="$1" + shift + if [[ "$GITLOGG_DEV" = "" ]]; then + # Run precompiled .compiled.js + node "${script%.es}.js" "$@" + else + # Development: transpile .js at runtime + babel "$script" | node "$@" + fi +} -workerFile='./scripts/output-intermediate-gitlog.sh' +workerFile="$__dirname/output-intermediate-gitlog.es" # define path to 'json' parser -jsonParser='./scripts/gitlogg-parse-json.js' - +jsonParser="$__dirname/gitlogg-parse-json.es" # Display system usage and exit usage() { + local exitCode="$1" cat <<\USAGE_EOF -usage: gitlogg-generate-log.sh [options] +usage: gitlogg [options] [repository ...] The following options are available -n X: Run X instances of gitlogg concurrently USAGE_EOF - exit 2 + exit $exitCode } test "$1" = "--help" && usage @@ -46,57 +38,26 @@ test $NUM_THREADS -lt 1 && NUM_THREADS=1 test "$1" = "-n" && NUM_THREADS=$2 echo -e "${Blu}Info: Calculating in $NUM_THREADS thread(s)${RCol}" -# ensure there's always a '/' at the end of the 'yourpath' variable, since its value can be changed by user. -case "$yourpath" in - */) - yourpathSanitized="${yourpath}" # no changes if there's already a slash at the end - syntax sugar - ;; - *) - yourpathSanitized="${yourpath}/" # add a slash at the end if there isn't already one - ;; -esac - -# 'thepath' sets the path to each repository under 'yourpath' (the trailing asterix [*/] represents all the repository folders). -thepath="${yourpathSanitized}*/" +# Each positional argument is a path to a git repository +repositories=("$@") - -# function to trim whitespace -trim() { - local var="$*" - var="${var#'${var%%[![:space:]]*}'}" # remove leading whitespace characters - var="${var%'${var##*[![:space:]]}'}" # remove trailing whitespace characters - echo -n "$var" -} - -# number of directories (repos) under 'thepath' -DIRCOUNT="$(find $thepath -maxdepth 0 -type d | wc -l)" - -# trim whitespace from DIRCOUNT -DIRNR="$(trim $DIRCOUNT)" +# number of directories (repos) +DIRCOUNT="${#repositories[@]}" # determine if we're dealing with a singular repo or multiple -if [ "${DIRNR}" -gt "1" ]; then - reporef="all ${Red}${DIRNR}${Yel} repositories" -elif [ "${DIRNR}" -eq "1" ]; then - reporef="the one repository" +if [ "${DIRCOUNT}" -gt "1" ]; then + reporef="${Red}${DIRCOUNT}${Yel} repositories" +elif [ "${DIRCOUNT}" -eq "1" ]; then + reporef="${Red}${DIRCOUNT}${Yel} repository" +elif [ "${DIRCOUNT}" -eq "0" ]; then + echo -e "${Whi}[ERROR 003]: ${Yel}No repositories specified${RCol}" 1>&2 + usage 1 1>&2 fi # start counting seconds elapsed SECONDS=0 -# if the path exists and is not empty -if [ -d "${yourpathSanitized}" ] && [ "$(ls $yourpathSanitized)" ]; then - echo -e "${Yel}Generating ${Pur}git log ${Yel}for ${reporef} located at ${Red}'${thepath}'${Yel}. ${Blu}This might take a while!${RCol}" - dirs=$(ls -d $thepath) - echo $dirs | xargs -n 1 -P $NUM_THREADS $workerFile > ${tempOutputFile} - echo -e "${Gre}The file ${Blu}${tempOutputFile} ${Gre}generated in${RCol}: ${SECONDS}s" && - babel "${jsonParser}" | node # only parse JSON if we have a source to parse it from -# if the path exists but is empty -elif [ -d "${yourpathSanitized}" ] && [ ! "$(ls $yourpathSanitized)" ]; then - echo -e "${Whi}[ERROR 002]: ${Yel}The path to the local repositories ${Red}'${yourpath}'${Yel}, which is set on the file ${Blu}'${thisFile}' ${UYel}exists, but is empty!${RCol}" - echo -e "${Yel}Please move the repos to ${Red}'${yourpath}'${Yel} or update the variable ${Pur}'yourpath'${Yel} to reflect the absolute path to the directory where the repos are located.${RCol}" -# if the path does not exists -elif [ ! -d "${yourpathSanitized}" ]; then - echo -e "${Whi}[ERROR 001]: ${Yel}The path to the local repositories ${Red}'${yourpath}'${Yel}, which is set on the file ${Blu}'${thisFile}' ${UYel}does not exist!${RCol}" - echo -e "${Yel}Please create ${Red}'${yourpath}'${Yel} and move the repos under it, or update the variable ${Pur}'yourpath'${Yel} to reflect the absolute path to the directory where the repos are located.${RCol}" -fi +echo -e "${Yel}Generating ${Pur}git log ${Yel}for ${reporef} located at ${Red}'${thepath}'${Yel}. ${Blu}This might take a while!${RCol}" +echo "${repositories[@]}" \ +| xargs -n 1 -P $NUM_THREADS "$workerFile" \ +| runJs "${jsonParser}" 3<&0 4>"gitlogg.json" # only parse JSON if we have a source to parse it from \ No newline at end of file diff --git a/scripts/gitlogg-parse-json.es b/scripts/gitlogg-parse-json.es new file mode 100644 index 0000000..64d6b75 --- /dev/null +++ b/scripts/gitlogg-parse-json.es @@ -0,0 +1,64 @@ +var fs = require('fs'), + path = require('path'), + chalk = require('chalk'), + {chunk} = require('lodash'), + {fields: gitLogFields} = require('./git-log-fields'), + // Read and write from file descriptors. Bash script will hook these up. + input_file = 3, + output_file = 4; + +try { + require('source-map-support').install(); +} catch(e) {} + +console.log(chalk.yellow('Generating JSON output...')); + +var changes = function(data, index) { + var v = data.split(',')[index] || 0; + var w = 0; + if (v !== 0) { + var w = v.split(' ')[1]; // the number of changes is second on the array + } + return parseInt(w); +}; + +console.time(chalk.green('JSON output generated in')); + + +const fields = [ + {identifier: 'repository'}, + ...gitLogFields, + {identifier: 'shortstats'} +]; + +let input = fs.readFileSync(input_file, 'utf8').split('\0'); +input.shift(); +const totalFields = fields.length; +const inputCommits = chunk(input, totalFields); + +const output = {}; +inputCommits.forEach(item => { + // The last field for each commit includes the trailing newline output by `git log`; remove it + item[totalFields - 1] = item[totalFields - 1].slice(0, -1); + + const commit = {}; + fields.forEach((field, i) => { + commit[field.identifier] = item[i]; + }); + const stats = commit.shortstats; + commit.files_changed = changes(stats, 0); + const insertions = commit.insertions = changes(stats, 1); + const deletions = commit.deletions = changes(stats, 2); + commit.impact = insertions - deletions; + // TODO reimplement commit_nr + + const repository = commit.repository; + output[repository] = output[repository] || []; + output[repository].push(commit); + }); + +console.timeEnd(chalk.green('JSON output generated in')); + +console.log(chalk.yellow('Writing output to file...')); + +fs.writeFileSync(output_file, JSON.stringify(output, null, 2)); diff --git a/scripts/gitlogg-parse-json.js b/scripts/gitlogg-parse-json.js deleted file mode 100644 index 9e8b9fa..0000000 --- a/scripts/gitlogg-parse-json.js +++ /dev/null @@ -1,147 +0,0 @@ -var fs = require('fs'), - path = require('path'), - chalk = require('chalk'), - output_file_temp = '_tmp/gitlogg.tmp', - output_file = '_output/gitlogg.json'; - -console.log(chalk.yellow('Generating JSON output...')); - -var changes = function(data, index) { - var v = data.split(',')[index] || 0; - var w = 0; - if (v !== 0) { - var w = v.split(' ')[1]; // the number of changes is second on the array - } - return parseInt(w); -}; - -console.time(chalk.green('JSON output generated in')); - -var output = fs.readFileSync(output_file_temp, 'utf8') - .trim() - .split('\n') - .map(line => line.split('\\t')) - .reduce((commits, item) => { - - // vars based on sequential values ( sanitise " to ' on fields that accept user input ) - var repository = item[3], // color-consolidator - commit_nr = parseInt(item[0], 10), // 3 - commit_hash = item[5], // 5109ad5a394a4873290ff7f7a38b7ca2e1b3b8e1 - commit_hash_abbreviated = item[7], // 5109ad5 - tree_hash = item[9], // a1606ea8d6e24e1c832b52cb9c04ae1df2242ed4 - tree_hash_abbreviated = item[11], // a1606ea - parent_hashes = item[13], // 7082fa621bf93503fe173d06ada3c6111054a62b - parent_hashes_abbreviated = item[15], // 7082fa6 - author_name = item[17].replace(/"/g, "'"), // Wallace Sidhrée - author_name_mailmap = item[19], // Wallace Sidhrée - author_email = item[21], // i@dreamyguy.com - author_email_mailmap = item[23], // i@dreamyguy.com - author_date = item[25], // Fri Jan 3 14:16:56 2014 +0100 - author_date_RFC2822 = item[27], // Fri, 3 Jan 2014 14:16:56 +0100 - author_date_relative = item[29], // 2 years, 5 months ago - author_date_unix_timestamp = item[31], // 1388755016 - author_date_iso_8601 = item[33], // 2014-01-03 14:16:56 +0100 - author_date_iso_8601_strict = item[35], // 2014-01-03T14:16:56+01:00 - committer_name = item[37].replace(/"/g, "'"), // Wallace Sidhrée - committer_name_mailmap = item[39], // Wallace Sidhrée - committer_email = item[41], // i@dreamyguy.com - committer_email_mailmap = item[43], // i@dreamyguy.com - committer_date = item[45], // Fri Jan 3 14:16:56 2014 +0100 - committer_date_RFC2822 = item[47], // Fri, 3 Jan 2014 14:16:56 +0100 - committer_date_relative = item[49], // 2 years, 5 months ago - committer_date_unix_timestamp = item[51], // 1388755016 - committer_date_iso_8601 = item[53], // 2014-01-03 14:16:56 +0100 - committer_date_iso_8601_strict = item[55], // 2014-01-03T14:16:56+01:00 - ref_names = item[57].replace(/"/g, "'"), // "" - ref_names_no_wrapping = item[59].replace(/"/g, "'"), // "" - encoding = item[61], // "" - subject = item[63].replace(/"/g, "'"), // Upgrade FontAwesome from 3.2.1 to 4.0.3" - subject_sanitized = item[65], // Upgrade-FontAwesome-from-3.2.1-to-4.0.3" - commit_notes = item[67].replace(/"/g, "'"), // "" - stats = item[69].slice(1); // ` 9 files changed, 507 insertions(+), 2102 deletions(-)` - // vars that require manipulation - var time_array = author_date.split(' '), // Fri Jan 3 14:16:56 2014 +0100 => [Fri, Jan, 3, 14:16:56, 2014, +0100] - time_array_clock = time_array[3].split(':'), // 14:16:56 => [14, 16, 56] - time_hour = parseInt(time_array_clock[0], 10), // [14, 16, 56] => 14 - time_minutes = parseInt(time_array_clock[1], 10), // [14, 16, 56] => 16 - time_seconds = parseInt(time_array_clock[2], 10), // [14, 16, 56] => 56 - time_gmt = time_array[5], // [Fri, Jan, 3, 14:16:56, 2014, +0100] => +0100 - date_array = author_date_iso_8601.split(' ')[0], // 2014-01-03 14:16:56 +0100 => 2014-01-03 - date_day_week = time_array[0], // [Fri, Jan, 3, 14:16:56, 2014, +0100] => Fri - date_iso_8601 = date_array, // 2014-01-03 - date_month_day = parseInt(date_array.split('-')[2], 10), // 2014-01-03 => [2014, 01, 03] => 03 - date_month_name = time_array[1], // [Fri, Jan, 3, 14:16:56, 2014, +0100] => Jan - date_month_number = parseInt(date_array.split('-')[1], 10), // 2014-01-03 => [2014, 01, 03] => 01 - date_year = time_array[4], // [Fri, Jan, 3, 14:16:56, 2014, +0100] => 2014 - files_changed = changes(stats, 0), // ` 9 files changed, 507 insertions(+), 2102 deletions(-)` => 9 - insertions = changes(stats, 1), // ` 9 files changed, 507 insertions(+), 2102 deletions(-)` => 507 - deletions = changes(stats, 2), // ` 9 files changed, 507 insertions(+), 2102 deletions(-)` => 2102 - impact = (insertions - deletions); // 507 - 2102 => -1595 - - commits[item[1]] = commits[item[1]] || []; - commits[item[1]].push({ - repository: repository, - commit_nr: commit_nr, - commit_hash: commit_hash, - // commit_hash_abbreviated: commit_hash_abbreviated, - // tree_hash: tree_hash, - // tree_hash_abbreviated: tree_hash_abbreviated, - // parent_hashes: parent_hashes, - // parent_hashes_abbreviated: parent_hashes_abbreviated, - author_name: author_name, - // author_name_mailmap: author_name_mailmap, - author_email: author_email, - // author_email_mailmap: author_email_mailmap, - author_date: author_date, - // author_date_RFC2822: author_date_RFC2822, - author_date_relative: author_date_relative, - author_date_unix_timestamp: author_date_unix_timestamp, - author_date_iso_8601: author_date_iso_8601, - // author_date_iso_8601_strict: author_date_iso_8601_strict, - // committer_name: committer_name, - // committer_name_mailmap: committer_name_mailmap, - // committer_email: committer_email, - // committer_email_mailmap: committer_email_mailmap, - // committer_date: committer_date, - // committer_date_RFC2822: committer_date_RFC2822, - // committer_date_relative: committer_date_relative, - // committer_date_unix_timestamp: committer_date_unix_timestamp, - // committer_date_iso_8601: committer_date_iso_8601, - // committer_date_iso_8601_strict: committer_date_iso_8601_strict, - // ref_names: ref_names, - // ref_names_no_wrapping: ref_names_no_wrapping, - // encoding: encoding, - subject: subject, - subject_sanitized: subject_sanitized, - // commit_notes: commit_notes, - stats: stats, - time_hour: time_hour, - time_minutes: time_minutes, - time_seconds: time_seconds, - time_gmt: time_gmt, - date_day_week: date_day_week, - date_month_day: date_month_day, - date_month_name: date_month_name, - date_month_number: date_month_number, - date_year: date_year, - date_iso_8601: date_iso_8601, - files_changed: files_changed, - insertions: insertions, - deletions: deletions, - impact: impact - }); - return commits; - }, {}); - -console.timeEnd(chalk.green('JSON output generated in')); - -console.log(chalk.yellow('Writing output to file...')); - -// console.log('output', JSON.stringify(output, null, 2)) // comment this out to see the output on the terminal as well - -fs.writeFile(output_file, JSON.stringify(output, null, 2), function(err) { - if(err) { - return console.log(chalk.red(err, 'Something went wrong, the file could not be written / saved')); - } - console.log(chalk.green('The file ' + chalk.blue(output_file) + ' was saved! ')); -}); diff --git a/scripts/gitlogg.sh b/scripts/gitlogg.sh index ad26c59..ba2f622 100755 --- a/scripts/gitlogg.sh +++ b/scripts/gitlogg.sh @@ -1,6 +1,10 @@ -#!/bin/bash +#!/usr/bin/env bash -my_dir="$(dirname "$0")" -cd $my_dir +# Magical incantation mimics node's __dirname +__dirname="$(cd "$( + dirname "$( + node -e 'var out=process.argv[1];try {out=require("path").resolve(out, "..", require("fs").readlinkSync(out))} catch(e) {} finally {console.log(out)}' "${BASH_SOURCE[0]}" + )" +)" && pwd)" -bash gitlogg-generate-log.sh $@ +source "$__dirname/gitlogg-generate-log.sh" diff --git a/scripts/output-intermediate-gitlog.es b/scripts/output-intermediate-gitlog.es new file mode 100755 index 0000000..bb56eae --- /dev/null +++ b/scripts/output-intermediate-gitlog.es @@ -0,0 +1,23 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +const childProcess = require('child_process'); +const chalk = require('chalk'); +const which = require('which'); +const {formatString} = require('./git-log-fields'); + +// Returns an intermediate representation of git log with the given repository to stdout +const dir = path.resolve(process.argv[2]); +const dirName = path.basename(dir); + +fs.existsSync(dir) || process.exit(1); + +console.error('Outputting ' + chalk.magenta(dirName)); + +childProcess.spawnSync(which.sync('git'), [ + '--no-pager', 'log', '--all', '--no-merges', '--shortstat', '--reverse', + `--pretty=tformat:%x00${ dirName }%x00${ formatString }` +], { + cwd: dir, + stdio: 'inherit' +}); \ No newline at end of file diff --git a/scripts/output-intermediate-gitlog.sh b/scripts/output-intermediate-gitlog.sh deleted file mode 100755 index f6532f6..0000000 --- a/scripts/output-intermediate-gitlog.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Returns an intermediate representation of git log with the given repository to stdout - -my_dir="$(dirname "$0")" -cd $my_dir - -source "colors.sh" - -cd .. - -test "$1" || exit 1 -dir=$1 - -cd $dir && - echo -e "${Whi}Outputting ${Pur}${PWD##*/}${RCol}" >&2 && - git log --all --no-merges --shortstat --reverse --pretty=format:'commits\trepository\t'"${PWD##*/}"'\tcommit_hash\t%H\tcommit_hash_abbreviated\t%h\ttree_hash\t%T\ttree_hash_abbreviated\t%t\tparent_hashes\t%P\tparent_hashes_abbreviated\t%p\tauthor_name\t%an\tauthor_name_mailmap\t%aN\tauthor_email\t%ae\tauthor_email_mailmap\t%aE\tauthor_date\t%ad\tauthor_date_RFC2822\t%aD\tauthor_date_relative\t%ar\tauthor_date_unix_timestamp\t%at\tauthor_date_iso_8601\t%ai\tauthor_date_iso_8601_strict\t%aI\tcommitter_name\t%cn\tcommitter_name_mailmap\t%cN\tcommitter_email\t%ce\tcommitter_email_mailmap\t%cE\tcommitter_date\t%cd\tcommitter_date_RFC2822\t%cD\tcommitter_date_relative\t%cr\tcommitter_date_unix_timestamp\t%ct\tcommitter_date_iso_8601\t%ci\tcommitter_date_iso_8601_strict\t%cI\tref_names\t%d\tref_names_no_wrapping\t%D\tencoding\t%e\tsubject\t%s\tsubject_sanitized\t%f\tcommit_notes\t%N\tstats\t' | - sed '/^[ \t]*$/d' | # remove all newlines/line-breaks, including those with empty spaces - tr '\n' 'ò' | # convert newlines/line-breaks to a character, so we can manipulate it without much trouble - tr '\r' ' ' | # replace carriage returns with a space, so we avoid new lines popping from placeholders that allow user input - sed 's/tòcommits/tòòcommits/g' | # because some commits have no stats, we have to create an extra line-break to make `paste -d ' ' - -` consistent - tr 'ò' '\n' | # bring back all line-breaks - sed '{ - N - s/[)]\n\ncommits/)\ - commits/g - }' | # some rogue mystical line-breaks need to go down to their knees and beg for mercy, which they're not getting - paste -d ' ' - - | # collapse lines so that the `shortstat` is merged with the rest of the commit data, on a single line - awk '{print NR"\\t",$0}' | # print line number in front of each line, along with the `\t` delimiter - sed 's/\\t\ commits\\trepo/\\t\commits\\trepo/g' # get rid of the one space that shouldn't be there -