Add support to generate charts in habits plugin (#26)
* Add charts to habits plugin * Dockerfile update * Add support for dflags
This commit is contained in:
15
.github/workflows/workflow.yml
vendored
15
.github/workflows/workflow.yml
vendored
@@ -165,6 +165,21 @@ jobs:
|
|||||||
base: ""
|
base: ""
|
||||||
plugins_errors_fatal: yes
|
plugins_errors_fatal: yes
|
||||||
plugin_habits: yes
|
plugin_habits: yes
|
||||||
|
plugin_habits_from: 5
|
||||||
|
|
||||||
|
- name: ${{ matrix.template }} > Plugin > Habits (charts)
|
||||||
|
uses: lowlighter/metrics@master
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.METRICS_TOKEN }}
|
||||||
|
dryrun: yes
|
||||||
|
repositories: 1
|
||||||
|
template: ${{ matrix.template }}
|
||||||
|
base: ""
|
||||||
|
plugins_errors_fatal: yes
|
||||||
|
plugin_habits: yes
|
||||||
|
plugin_habits_from: 5
|
||||||
|
plugin_habits_facts: no
|
||||||
|
plugin_habits_charts: yes
|
||||||
|
|
||||||
- name: ${{ matrix.template }} > Plugin > Languages
|
- name: ${{ matrix.template }} > Plugin > Languages
|
||||||
uses: lowlighter/metrics@master
|
uses: lowlighter/metrics@master
|
||||||
|
|||||||
30
Dockerfile
30
Dockerfile
@@ -1,25 +1,29 @@
|
|||||||
# Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
|
|
||||||
|
|
||||||
# Base image
|
# Base image
|
||||||
FROM node:15-slim
|
FROM node:15-buster-slim
|
||||||
|
|
||||||
# Install latest chrome dev package and fonts to support major charsets
|
# Copy GitHub action
|
||||||
RUN apt-get update \
|
COPY action/dist/index.js /index.js
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
RUN chmod +x /index.js \
|
||||||
|
# Install latest chrome dev package, fonts to support major charsets and skip chromium download on puppeteer install
|
||||||
|
# Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
|
||||||
|
&& apt-get update \
|
||||||
&& apt-get install -y wget gnupg ca-certificates libgconf-2-4 \
|
&& apt-get install -y wget gnupg ca-certificates libgconf-2-4 \
|
||||||
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||||
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
||||||
--no-install-recommends \
|
--no-install-recommends \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
# Install ruby to support linguist
|
||||||
# Uncomment to skip the chromium download when installing puppeteer
|
# Based on https://github.com/github/linguist
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y ruby-full \
|
||||||
|
&& apt-get install -y git g++ cmake pkg-config libicu-dev zlib1g-dev libcurl4-openssl-dev libssl-dev ruby-dev \
|
||||||
|
&& gem install github-linguist
|
||||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||||
ENV PUPPETEER_BROWSER_PATH "google-chrome-stable"
|
ENV PUPPETEER_BROWSER_PATH "google-chrome-stable"
|
||||||
|
|
||||||
# Copy action
|
# Execute GitHub action
|
||||||
COPY action/dist/index.js /index.js
|
|
||||||
RUN chmod +x /index.js
|
|
||||||
|
|
||||||
# Execute action
|
|
||||||
ENTRYPOINT node /index.js
|
ENTRYPOINT node /index.js
|
||||||
47
action.yml
47
action.yml
@@ -98,16 +98,32 @@ inputs:
|
|||||||
default: no
|
default: no
|
||||||
|
|
||||||
# Coding habits plugin
|
# Coding habits plugin
|
||||||
# Search in your recent activity what've recently did and deduce tidbits like if you're using spaces or tabs as indents, etc.
|
# Search in your recent activity what've recently did and deduce facts/charts
|
||||||
plugin_habits:
|
plugin_habits:
|
||||||
description: Enable coding habits metrics
|
description: Enable coding habits metrics
|
||||||
default: no
|
default: no
|
||||||
|
|
||||||
# Number of activity events to base habits on
|
# Number of activity events to base habits on
|
||||||
# Maximum number of events is capped to 100
|
# Capped to 1000
|
||||||
plugin_habits_from:
|
plugin_habits_from:
|
||||||
description: Number of activity events to use
|
description: Number of activity events to use
|
||||||
default: 100
|
default: 200
|
||||||
|
|
||||||
|
# Number of days to base habits on (older events will be discarded)
|
||||||
|
# Capped to 30
|
||||||
|
plugin_habits_days:
|
||||||
|
description: Number of days to use
|
||||||
|
default: 14
|
||||||
|
|
||||||
|
# Display tidbits about your active hours/days, indent used (spaces/tabs), etc. deduced from recent activity
|
||||||
|
plugin_habits_facts:
|
||||||
|
description: Display habits facts based on recent activity
|
||||||
|
default: yes
|
||||||
|
|
||||||
|
# Display charts of most active time of the day and languages recently used
|
||||||
|
plugin_habits_charts:
|
||||||
|
description: Display recent coding activity charts
|
||||||
|
default: no
|
||||||
|
|
||||||
# Languages plugins
|
# Languages plugins
|
||||||
# Compute the most used programming languages on your repositories
|
# Compute the most used programming languages on your repositories
|
||||||
@@ -266,10 +282,10 @@ inputs:
|
|||||||
description: Number of tweets to display
|
description: Number of tweets to display
|
||||||
default: 2
|
default: 2
|
||||||
|
|
||||||
# Enable debug mode
|
# When enabled, any plugins errors will throw
|
||||||
# Ensure you correctly put all sensitive informations in your repository secrets before !
|
# By default, metrics are still generated with an error message
|
||||||
debug:
|
plugins_errors_fatal:
|
||||||
description: Enable debug logs
|
description: Die on plugins errors
|
||||||
default: no
|
default: no
|
||||||
|
|
||||||
# Verify SVG after generation
|
# Verify SVG after generation
|
||||||
@@ -278,14 +294,19 @@ inputs:
|
|||||||
description: Verify SVG after genaration
|
description: Verify SVG after genaration
|
||||||
default: no
|
default: no
|
||||||
|
|
||||||
|
# Enable debug mode
|
||||||
|
# Ensure you correctly put all sensitive informations in your repository secrets before !
|
||||||
|
debug:
|
||||||
|
description: Enable debug logs
|
||||||
|
default: no
|
||||||
|
|
||||||
|
# Debug flags (used for testing)
|
||||||
|
debug_flags:
|
||||||
|
description: Debug flags
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Enable dry-run mode
|
# Enable dry-run mode
|
||||||
# Generate image but does not push it (used for testing)
|
# Generate image but does not push it (used for testing)
|
||||||
dryrun:
|
dryrun:
|
||||||
description: Enable dry-run
|
description: Enable dry-run
|
||||||
default: no
|
default: no
|
||||||
|
|
||||||
# When enabled, any plugins errors will throw
|
|
||||||
# By default, metrics are still generated with an error message
|
|
||||||
plugins_errors_fatal:
|
|
||||||
description: Die on plugins errors
|
|
||||||
default: no
|
|
||||||
10
action/dist/index.js
vendored
10
action/dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -71,6 +71,8 @@
|
|||||||
if (!debug)
|
if (!debug)
|
||||||
console.debug = message => debugged.push(message)
|
console.debug = message => debugged.push(message)
|
||||||
console.log(`Debug mode | ${debug}`)
|
console.log(`Debug mode | ${debug}`)
|
||||||
|
const dflags = (core.getInput("debug_flags") ?? "").split(" ").filter(flag => flag)
|
||||||
|
console.log(`Debug flags | ${dflags.join(" ")}`)
|
||||||
|
|
||||||
//Base elements
|
//Base elements
|
||||||
const base = {}
|
const base = {}
|
||||||
@@ -84,7 +86,7 @@
|
|||||||
lines:{enabled:bool(core.getInput("plugin_lines"))},
|
lines:{enabled:bool(core.getInput("plugin_lines"))},
|
||||||
traffic:{enabled:bool(core.getInput("plugin_traffic"))},
|
traffic:{enabled:bool(core.getInput("plugin_traffic"))},
|
||||||
pagespeed:{enabled:bool(core.getInput("plugin_pagespeed"))},
|
pagespeed:{enabled:bool(core.getInput("plugin_pagespeed"))},
|
||||||
habits:{enabled:bool(core.getInput("plugin_habits")), from:Number(core.getInput("plugin_habits_from")) || 100},
|
habits:{enabled:bool(core.getInput("plugin_habits"))},
|
||||||
languages:{enabled:bool(core.getInput("plugin_languages"))},
|
languages:{enabled:bool(core.getInput("plugin_languages"))},
|
||||||
followup:{enabled:bool(core.getInput("plugin_followup"))},
|
followup:{enabled:bool(core.getInput("plugin_followup"))},
|
||||||
music:{enabled:bool(core.getInput("plugin_music"))},
|
music:{enabled:bool(core.getInput("plugin_music"))},
|
||||||
@@ -112,6 +114,15 @@
|
|||||||
console.log(`Languages ignored | ${q["languages.ignored"]}`)
|
console.log(`Languages ignored | ${q["languages.ignored"]}`)
|
||||||
console.log(`Languages skipped repos | ${q["languages.skipped"]}`)
|
console.log(`Languages skipped repos | ${q["languages.skipped"]}`)
|
||||||
}
|
}
|
||||||
|
//Habits
|
||||||
|
if (plugins.habits.enabled) {
|
||||||
|
for (const option of ["from", "days", "facts", "charts"])
|
||||||
|
q[`habits.${option}`] = core.getInput(`plugin_habits_${option}`) || null
|
||||||
|
console.log(`Habits facts | ${q["habits.facts"]}`)
|
||||||
|
console.log(`Habits charts | ${q["habits.charts"]}`)
|
||||||
|
console.log(`Habits events to use | ${q["habits.from"]}`)
|
||||||
|
console.log(`Habits days to keep | ${q["habits.days"]}`)
|
||||||
|
}
|
||||||
//Music
|
//Music
|
||||||
if (plugins.music.enabled) {
|
if (plugins.music.enabled) {
|
||||||
plugins.music.token = core.getInput("plugin_music_token") || ""
|
plugins.music.token = core.getInput("plugin_music_token") || ""
|
||||||
@@ -169,7 +180,7 @@
|
|||||||
q = {...q, base:false, ...base, repositories, template}
|
q = {...q, base:false, ...base, repositories, template}
|
||||||
|
|
||||||
//Render metrics
|
//Render metrics
|
||||||
const rendered = await metrics({login:user, q}, {graphql, rest, plugins, conf, die})
|
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die})
|
||||||
console.log(`Render | complete`)
|
console.log(`Render | complete`)
|
||||||
|
|
||||||
//Verify svg
|
//Verify svg
|
||||||
|
|||||||
@@ -48,6 +48,9 @@
|
|||||||
"languages.skipped":"",
|
"languages.skipped":"",
|
||||||
"pagespeed.detailed":false,
|
"pagespeed.detailed":false,
|
||||||
"habits.from":100,
|
"habits.from":100,
|
||||||
|
"habits.days":14,
|
||||||
|
"habits.facts":true,
|
||||||
|
"habits.charts":false,
|
||||||
"music.playlist":"",
|
"music.playlist":"",
|
||||||
"music.limit":4,
|
"music.limit":4,
|
||||||
"posts.limit":4,
|
"posts.limit":4,
|
||||||
|
|||||||
@@ -94,7 +94,19 @@
|
|||||||
<h4>{{ plugins.descriptions.habits }}</h4>
|
<h4>{{ plugins.descriptions.habits }}</h4>
|
||||||
<label>
|
<label>
|
||||||
Number of events for habits
|
Number of events for habits
|
||||||
<input type="number" v-model="plugins.options['habits.from']" min="1" max="100">
|
<input type="number" v-model="plugins.options['habits.from']" min="1" max="1000">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Number of days for habits
|
||||||
|
<input type="number" v-model="plugins.options['habits.days']" min="1" max="30">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Display tidbits
|
||||||
|
<input type="checkbox" v-model="plugins.options['habits.facts']" @change="load">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Display activity charts
|
||||||
|
<input type="checkbox" v-model="plugins.options['habits.charts']" @change="load">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="options-group" v-if="plugins.enabled.posts">
|
<div class="options-group" v-if="plugins.enabled.posts">
|
||||||
|
|||||||
@@ -7,9 +7,13 @@
|
|||||||
import Templates from "./templates/index.mjs"
|
import Templates from "./templates/index.mjs"
|
||||||
import puppeteer from "puppeteer"
|
import puppeteer from "puppeteer"
|
||||||
import url from "url"
|
import url from "url"
|
||||||
|
import processes from "child_process"
|
||||||
|
import fs from "fs/promises"
|
||||||
|
import os from "os"
|
||||||
|
import paths from "path"
|
||||||
|
|
||||||
//Setup
|
//Setup
|
||||||
export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false}) {
|
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false}) {
|
||||||
//Compute rendering
|
//Compute rendering
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -49,7 +53,7 @@
|
|||||||
//Compute metrics
|
//Compute metrics
|
||||||
console.debug(`metrics/compute/${login} > compute`)
|
console.debug(`metrics/compute/${login} > compute`)
|
||||||
const computer = Templates[template].default || Templates[template]
|
const computer = Templates[template].default || Templates[template]
|
||||||
await computer({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, format, bytes, shuffle, htmlescape, urlexpand}})
|
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, format, bytes, shuffle, htmlescape, urlexpand}})
|
||||||
const promised = await Promise.all(pending)
|
const promised = await Promise.all(pending)
|
||||||
|
|
||||||
//Check plugins errors
|
//Check plugins errors
|
||||||
@@ -133,6 +137,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Run command */
|
||||||
|
async function run(command, options) {
|
||||||
|
return await new Promise((solve, reject) => {
|
||||||
|
console.debug(`metrics/command > ${command}`)
|
||||||
|
const child = processes.exec(command, options)
|
||||||
|
let [stdout, stderr] = ["", ""]
|
||||||
|
child.stdout.on("data", data => stdout += data)
|
||||||
|
child.stderr.on("data", data => stderr += data)
|
||||||
|
child.on("close", code => {
|
||||||
|
console.debug(`metrics/command > ${command} > exited with code ${code}`)
|
||||||
|
return code === 0 ? solve(stdout) : reject(stderr)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** Placeholder generator */
|
/** Placeholder generator */
|
||||||
function placeholder({data, conf, q}) {
|
function placeholder({data, conf, q}) {
|
||||||
//Proxifier
|
//Proxifier
|
||||||
@@ -175,7 +194,7 @@
|
|||||||
music:{provider:"########", tracks:new Array("music.limit" in q ? Math.max(Number(q["music.limit"])||0, 0) : 4).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})},
|
music:{provider:"########", tracks:new Array("music.limit" in q ? Math.max(Number(q["music.limit"])||0, 0) : 4).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})},
|
||||||
pagespeed:{detailed:!!q["pagespeed.detailed"], scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))},
|
pagespeed:{detailed:!!q["pagespeed.detailed"], scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))},
|
||||||
followup:{issues:{count:0}, pr:{count:0}},
|
followup:{issues:{count:0}, pr:{count:0}},
|
||||||
habits:{indents:{style:`########`}},
|
habits:{facts:!!(q["habits.facts"] ?? 1), charts:!!q["habits.charts"], indents:{style:`########`}, commits:{day:"####"}, linguist:{ordered:[]}},
|
||||||
languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))},
|
languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))},
|
||||||
topics:{list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
|
topics:{list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
|
||||||
projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]},
|
projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]},
|
||||||
|
|||||||
@@ -1,46 +1,99 @@
|
|||||||
//Setup
|
//Setup
|
||||||
export default async function ({login, rest, q}, {enabled = false, from:defaults = 100} = {}) {
|
export default async function ({login, rest, imports, q}, {enabled = false, from:defaults = 100} = {}) {
|
||||||
//Plugin execution
|
//Plugin execution
|
||||||
try {
|
try {
|
||||||
//Check if plugin is enabled and requirements are met
|
//Check if plugin is enabled and requirements are met
|
||||||
if ((!enabled)||(!q.habits))
|
if ((!enabled)||(!q.habits))
|
||||||
return null
|
return null
|
||||||
//Parameters override
|
//Parameters override
|
||||||
let {"habits.from":from = defaults.from ?? 100} = q
|
let {"habits.from":from = defaults.from ?? 500, "habits.days":days = 30, "habits.facts":facts = true, "habits.charts":charts = false} = q
|
||||||
//Events
|
//Events
|
||||||
from = Math.max(1, Math.min(100, Number(from)))
|
from = Math.max(1, Math.min(1000, Number(from)))
|
||||||
|
//Days
|
||||||
|
days = Math.max(1, Math.min(30, Number(from)))
|
||||||
//Initialization
|
//Initialization
|
||||||
const habits = {commits:{hour:NaN, hours:{}}, indents:{style:"", spaces:0, tabs:0}}
|
const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}}
|
||||||
//Get user recent commits from events
|
const pages = Math.ceil(from/100)
|
||||||
const events = await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:from})
|
//Get user recent activity
|
||||||
const commits = events.data
|
const events = []
|
||||||
|
try {
|
||||||
|
for (let page = 0; page < pages; page++) {
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > loaded page ${page}`)
|
||||||
|
events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data)
|
||||||
|
}
|
||||||
|
} catch { console.debug(`metrics/compute/${login}/plugins > habits > no more events to load`) }
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > no more events to load (${events.length} loaded)`)
|
||||||
|
//Get user recent commits
|
||||||
|
const commits = events
|
||||||
.filter(({type}) => type === "PushEvent")
|
.filter(({type}) => type === "PushEvent")
|
||||||
.filter(({actor}) => actor.login === login)
|
.filter(({actor}) => actor.login === login)
|
||||||
|
.filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000))
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} commits`)
|
||||||
|
const actor = commits[0]?.actor?.id ?? 0
|
||||||
|
//Retrieve edited files and filter edited lines (those starting with +/-) from patches
|
||||||
|
const patches = [...await Promise.allSettled(commits
|
||||||
|
.flatMap(({payload}) => payload.commits).map(commit => commit.url)
|
||||||
|
.map(async commit => (await rest.request(commit)).data.files)
|
||||||
|
)]
|
||||||
|
.filter(({status}) => status === "fulfilled")
|
||||||
|
.map(({value}) => value)
|
||||||
|
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), patch:file.patch ?? ""})))
|
||||||
|
.map(({name, patch}) => ({name, patch:patch.split("\n").filter(line => /^[-+]/.test(line)).map(line => line.substring(1)).join("\n")}))
|
||||||
|
//Commit day
|
||||||
|
{
|
||||||
|
//Compute commit days
|
||||||
|
const days = commits.map(({created_at}) => (new Date(created_at)).getDay())
|
||||||
|
for (const day of days)
|
||||||
|
habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1
|
||||||
|
habits.commits.days.max = Math.max(...Object.values(habits.commits.days))
|
||||||
|
//Compute day with most commits
|
||||||
|
habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([an, a], [bn, b]) => b - a).map(([day, occurence]) => day)[0]] ?? NaN : NaN
|
||||||
|
}
|
||||||
//Commit hour
|
//Commit hour
|
||||||
{
|
{
|
||||||
//Compute commit hours
|
//Compute commit hours
|
||||||
const hours = commits.map(({created_at}) => (new Date(created_at)).getHours())
|
const hours = commits.map(({created_at}) => (new Date(created_at)).getHours())
|
||||||
for (const hour of hours)
|
for (const hour of hours)
|
||||||
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
|
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
|
||||||
|
habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours))
|
||||||
//Compute hour with most commits
|
//Compute hour with most commits
|
||||||
habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0]}`.padStart(2, "0") : NaN
|
habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0]}`.padStart(2, "0") : NaN
|
||||||
}
|
}
|
||||||
//Indent style
|
//Indent style
|
||||||
{
|
{
|
||||||
//Retrieve edited files
|
//Attempt to guess whether tabs or spaces are used in patches
|
||||||
const edited = await Promise.allSettled(commits
|
patches
|
||||||
.flatMap(({payload}) => payload.commits).map(commit => commit.url)
|
.map(({patch}) => patch.match(/((?:\t)|(?: )) /gm) ?? [])
|
||||||
.map(async commit => (await rest.request(commit)).data.files)
|
|
||||||
)
|
|
||||||
//Attemp to guess whether tabs or spaces are used from patch
|
|
||||||
edited
|
|
||||||
.filter(({status}) => status === "fulfilled")
|
|
||||||
.map(({value}) => value)
|
|
||||||
.flatMap(files => files.flatMap(file => (file.patch ?? "").match(/(?<=^[+])((?:\t)|(?: )) /gm) ?? []))
|
|
||||||
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
||||||
//Compute indent style
|
|
||||||
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""
|
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""
|
||||||
}
|
}
|
||||||
|
//Linguist
|
||||||
|
if (charts) {
|
||||||
|
//Check if linguist exists
|
||||||
|
const prefix = {win32:"wsl"}[process.platform] ?? ""
|
||||||
|
if (await imports.run(`${prefix} which github-linguist`)) {
|
||||||
|
//Setup for linguist
|
||||||
|
habits.linguist.available = true
|
||||||
|
const path = imports.paths.join(imports.os.tmpdir(), `${actor}`)
|
||||||
|
//Create temporary directory and save patches
|
||||||
|
await imports.fs.mkdir(path, {recursive:true})
|
||||||
|
await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > created temp dir ${path} with ${patches.length} files`)
|
||||||
|
//Create temporary git repository
|
||||||
|
await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "null@github.com" && git commit -m "linguist"`, {cwd:path}).catch(console.debug)
|
||||||
|
await imports.run(`git status`, {cwd:path})
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > created temp git repository`)
|
||||||
|
//Spawn linguist process
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
|
||||||
|
;(await imports.run(`${prefix} github-linguist`, {cwd:path}))
|
||||||
|
//Parse linguist result
|
||||||
|
.split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>\w+)/)?.groups).filter(line => line)
|
||||||
|
.map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100)
|
||||||
|
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([an, a], [bn, b]) => b - a)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
console.debug(`metrics/compute/${login}/plugins > habits > linguist is not available`)
|
||||||
|
}
|
||||||
//Results
|
//Results
|
||||||
return habits
|
return habits
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
+ ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16
|
+ ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16
|
||||||
+ (!!plugins.followup)*68
|
+ (!!plugins.followup)*68
|
||||||
+ (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20
|
+ (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20
|
||||||
+ (!!plugins.habits)*68
|
+ (!!plugins.habits)*28 + (!!plugins.habits?.facts)*58 + (!!plugins.habits?.charts)*226
|
||||||
+ (!!plugins.languages)*96
|
+ (!!plugins.languages)*96
|
||||||
+ (!!plugins.music)*64 + (plugins.music?.tracks?.length ? 14+Math.max(0, plugins.music.tracks.length-1)*36 : 0)
|
+ (!!plugins.music)*64 + (plugins.music?.tracks?.length ? 14+Math.max(0, plugins.music.tracks.length-1)*36 : 0)
|
||||||
+ (!!plugins.posts)*64 + (plugins.posts?.list?.length ?? 0)*40
|
+ (!!plugins.posts)*64 + (plugins.posts?.list?.length ?? 0)*40
|
||||||
@@ -504,8 +504,9 @@
|
|||||||
<section>
|
<section>
|
||||||
<h2 class="field">
|
<h2 class="field">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 01-1.484.211c-.04-.282-.163-.547-.37-.847a8.695 8.695 0 00-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.75.75 0 01-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75zM6 15.25a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5h-2.5a.75.75 0 01-.75-.75zM5.75 12a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 01-1.484.211c-.04-.282-.163-.547-.37-.847a8.695 8.695 0 00-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.75.75 0 01-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75zM6 15.25a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5h-2.5a.75.75 0 01-.75-.75zM5.75 12a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5z"></path></svg>
|
||||||
Coding habits
|
Coding habits and recent activity
|
||||||
</h2>
|
</h2>
|
||||||
|
<% if (plugins.habits.facts) { %>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<% if (plugins.habits.error) { %>
|
<% if (plugins.habits.error) { %>
|
||||||
<section>
|
<section>
|
||||||
@@ -522,10 +523,62 @@
|
|||||||
<% if (!Number.isNaN(plugins.habits.commits.hour)) { %>
|
<% if (!Number.isNaN(plugins.habits.commits.hour)) { %>
|
||||||
<li>Mostly push code around <%= plugins.habits.commits.hour %>:00</li>
|
<li>Mostly push code around <%= plugins.habits.commits.hour %>:00</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (plugins.habits.commits.day) { %>
|
||||||
|
<li>Mostly active on <%= plugins.habits.commits.day.toLocaleLowerCase() %></li>
|
||||||
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<% if (plugins.habits.charts) { %>
|
||||||
|
<% if (!Number.isNaN(plugins.habits.commits.hour)) { %>
|
||||||
|
<section class="column chart">
|
||||||
|
<h3>Commit activity per time of the day</h3>
|
||||||
|
<div class="chart-bars">
|
||||||
|
<% for (let h = 0; h < 24; h++) { const p = (plugins.habits.commits.hours[h]??0)/(plugins.habits.commits.hours.max??1); %>
|
||||||
|
<div class="entry">
|
||||||
|
<span class="value"><%= plugins.habits.commits.hours[h] %></span>
|
||||||
|
<div class="bar" style="height: <%= p*50 %>px; background-color: var(--color-calendar-graph-day-L<%= Math.ceil(p/0.25) %>-bg)"></div>
|
||||||
|
<%= `${h}`.padStart(2, 0) %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<% if (!Number.isNaN(plugins.habits.commits.day)) { %>
|
||||||
|
<section class="column chart">
|
||||||
|
<h3>Commit activity per day</h3>
|
||||||
|
<div class="chart-bars">
|
||||||
|
<% for (let d = 0; d < 7; d++) { const p = (plugins.habits.commits.days[d]??0)/(plugins.habits.commits.days.max??1); %>
|
||||||
|
<div class="entry">
|
||||||
|
<span class="value"><%= plugins.habits.commits.days[d] %></span>
|
||||||
|
<div class="bar" style="height: <%= p*50 %>px; background-color: var(--color-calendar-graph-day-L<%= Math.ceil(p/0.25) %>-bg)"></div>
|
||||||
|
<%= ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d] %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
<% if (plugins.habits.linguist.available) { %>
|
||||||
|
<section class="column chart">
|
||||||
|
<h3>Language activity</h3>
|
||||||
|
<div class="chart-bars horizontal">
|
||||||
|
<% for (const [language, p] of plugins.habits.linguist.ordered) { %>
|
||||||
|
<div class="entry">
|
||||||
|
<span class="name"><%= language %></span>
|
||||||
|
<div class="bar" style="width: <%= p*80 %>%; background-color: var(--color-calendar-graph-day-L<%= Math.ceil(p/0.25) %>-bg)"></div>
|
||||||
|
<span class="value"><%= Math.round(100*p) %>%</span>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (plugins.topics) { %>
|
<% if (plugins.topics) { %>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 73 KiB |
@@ -304,6 +304,63 @@
|
|||||||
color: #666666;
|
color: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Charts and graphs */
|
||||||
|
.chart {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars .entry {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars .entry .value {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars .bar {
|
||||||
|
width: 7px;
|
||||||
|
background-color: var(--color-calendar-graph-day-bg);
|
||||||
|
border: 1px solid var(--color-calendar-graph-day-border);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars.horizontal {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: space-between;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars.horizontal .entry {
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars.horizontal .entry .name {
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
min-width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-bars.horizontal .bar {
|
||||||
|
height: 7px;
|
||||||
|
width: auto;
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Fade animation */
|
/* Fade animation */
|
||||||
.af {
|
.af {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** Template common processor */
|
/** Template common processor */
|
||||||
export default async function ({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
|
export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports}) {
|
||||||
|
|
||||||
//Init
|
//Init
|
||||||
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
|
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
|
||||||
@@ -61,4 +61,14 @@
|
|||||||
//Meta
|
//Meta
|
||||||
data.meta = {version:conf.package.version, author:conf.package.author}
|
data.meta = {version:conf.package.version, author:conf.package.author}
|
||||||
|
|
||||||
|
//Debug flags
|
||||||
|
if (dflags.includes("--cakeday")||q["dflag.cakeday"]) {
|
||||||
|
console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
|
||||||
|
computed.cakeday = true
|
||||||
|
}
|
||||||
|
if (dflags.includes("--hireable")||q["dflag.hireable"]) {
|
||||||
|
console.debug(`metrics/compute/${login} > applying dflag --hireable`)
|
||||||
|
data.user.isHireable = true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,16 @@ jobs:
|
|||||||
uses: lowlighter/metrics@<%- release %>
|
uses: lowlighter/metrics@<%- release %>
|
||||||
<%- testcase({
|
<%- testcase({
|
||||||
plugin_habits: true,
|
plugin_habits: true,
|
||||||
|
plugin_habits_from: 5,
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
- name: ${{ matrix.template }} > Plugin > Habits (charts)
|
||||||
|
uses: lowlighter/metrics@<%- release %>
|
||||||
|
<%- testcase({
|
||||||
|
plugin_habits: true,
|
||||||
|
plugin_habits_from: 5,
|
||||||
|
plugin_habits_facts: false,
|
||||||
|
plugin_habits_charts: true,
|
||||||
}) %>
|
}) %>
|
||||||
|
|
||||||
- name: ${{ matrix.template }} > Plugin > Languages
|
- name: ${{ matrix.template }} > Plugin > Languages
|
||||||
|
|||||||
Reference in New Issue
Block a user