@vladimirshefer/git-stats 0.0.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +510 -504
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -31,18 +31,46 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
// src/index.ts
|
|
32
32
|
var index_exports = {};
|
|
33
33
|
__export(index_exports, {
|
|
34
|
-
|
|
34
|
+
dataDir: () => dataDir,
|
|
35
|
+
progress: () => progress,
|
|
36
|
+
runScan1: () => runScan1
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
39
|
+
var fs5 = __toESM(require("fs"));
|
|
40
|
+
var path5 = __toESM(require("path"));
|
|
41
|
+
var readline = __toESM(require("readline"));
|
|
42
|
+
|
|
43
|
+
// src/output/report_template.ts
|
|
44
|
+
var path = __toESM(require("path"));
|
|
45
|
+
var fs = __toESM(require("fs"));
|
|
46
|
+
|
|
47
|
+
// ../html-ui/dist/index.html
|
|
48
|
+
var dist_default = '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <script src="https://cdn.plot.ly/plotly-3.3.0.min.js" charset="utf-8"></script>\n <title>Git Blame Statistics</title>\n <style>/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-4xl:56rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--font-weight-normal:400;--font-weight-semibold:600;--radius-md:.375rem;--radius-lg:.5rem;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.z-10{z-index:10}.float-right{float:right}.m-0{margin:calc(var(--spacing)*0)}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-8{margin-top:calc(var(--spacing)*8)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-1\\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.block{display:block}.flex{display:flex}.grid{display:grid}.h-4{height:calc(var(--spacing)*4)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-80{max-height:calc(var(--spacing)*80)}.w-4{width:calc(var(--spacing)*4)}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\\[repeat\\(auto-fit\\,minmax\\(220px\\,1fr\\)\\)\\]{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.items-center{align-items:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.px-3{padding-inline:calc(var(--spacing)*3)}.py-1\\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-6{padding-block:calc(var(--spacing)*6)}.pb-2\\.5{padding-bottom:calc(var(--spacing)*2.5)}.text-center{text-align:center}.text-left{text-align:left}.font-sans{font-family:var(--font-sans)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-blue-600{color:var(--color-blue-600)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-white{color:var(--color-white)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (hover:hover){.hover\\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\\:bg-gray-700:hover{background-color:var(--color-gray-700)}}.focus\\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.focus\\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:48rem){.md\\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}</style>\n</head>\n<script>\n try {\n window.RAW_DATASET =\n __DATASET_JSON__\n } catch (e) {\n window.RAW_DATASET = [[1, 2, 3, 4, 5, 6, 7, 8, 9]]\n }\n</script>\n<body class="font-sans m-0 bg-gray-50 text-gray-900">\n <div id="root"></div>\n <script>(()=>{var Y,g,he,ze,U,fe,ve,ge,ye,oe,te,ne,Qe,Q={},G=[],Ge=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,J=Array.isArray;function E(t,e){for(var n in e)t[n]=e[n];return t}function _e(t){t&&t.parentNode&&t.parentNode.removeChild(t)}function Xe(t,e,n){var _,s,o,l={};for(o in e)o=="key"?_=e[o]:o=="ref"?s=e[o]:l[o]=e[o];if(arguments.length>2&&(l.children=arguments.length>3?Y.call(arguments,2):n),typeof t=="function"&&t.defaultProps!=null)for(o in t.defaultProps)l[o]===void 0&&(l[o]=t.defaultProps[o]);return q(t,l,_,s,null)}function q(t,e,n,_,s){var o={type:t,props:e,key:n,ref:_,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s==null?++he:s,__i:-1,__u:0};return s==null&&g.vnode!=null&&g.vnode(o),o}function H(t){return t.children}function z(t,e){this.props=t,this.context=e}function F(t,e){if(e==null)return t.__?F(t.__,t.__i+1):null;for(var n;e<t.__k.length;e++)if((n=t.__k[e])!=null&&n.__e!=null)return n.__e;return typeof t.type=="function"?F(t):null}function Ye(t){if(t.__P&&t.__d){var e=t.__v,n=e.__e,_=[],s=[],o=E({},e);o.__v=e.__v+1,g.vnode&&g.vnode(o),se(t.__P,o,e,t.__n,t.__P.namespaceURI,32&e.__u?[n]:null,_,n==null?F(e):n,!!(32&e.__u),s),o.__v=e.__v,o.__.__k[o.__i]=o,ke(_,o,s),e.__e=e.__=null,o.__e!=n&&be(o)}}function be(t){if((t=t.__)!=null&&t.__c!=null)return t.__e=t.__c.base=null,t.__k.some(function(e){if(e!=null&&e.__e!=null)return t.__e=t.__c.base=e.__e}),be(t)}function pe(t){(!t.__d&&(t.__d=!0)&&U.push(t)&&!X.__r++||fe!=g.debounceRendering)&&((fe=g.debounceRendering)||ve)(X)}function X(){for(var t,e=1;U.length;)U.length>e&&U.sort(ge),t=U.shift(),e=U.length,Ye(t);X.__r=0}function xe(t,e,n,_,s,o,l,a,f,i,u){var r,p,c,m,y,b,v,h=_&&_.__k||G,C=e.length;for(f=Je(n,e,h,f,C),r=0;r<C;r++)(c=n.__k[r])!=null&&(p=c.__i!=-1&&h[c.__i]||Q,c.__i=r,b=se(t,c,p,s,o,l,a,f,i,u),m=c.__e,c.ref&&p.ref!=c.ref&&(p.ref&&le(p.ref,null,c),u.push(c.ref,c.__c||m,c)),y==null&&m!=null&&(y=m),(v=!!(4&c.__u))||p.__k===c.__k?f=we(c,f,t,v):typeof c.type=="function"&&b!==void 0?f=b:m&&(f=m.nextSibling),c.__u&=-7);return n.__e=y,f}function Je(t,e,n,_,s){var o,l,a,f,i,u=n.length,r=u,p=0;for(t.__k=new Array(s),o=0;o<s;o++)(l=e[o])!=null&&typeof l!="boolean"&&typeof l!="function"?(typeof l=="string"||typeof l=="number"||typeof l=="bigint"||l.constructor==String?l=t.__k[o]=q(null,l,null,null,null):J(l)?l=t.__k[o]=q(H,{children:l},null,null,null):l.constructor===void 0&&l.__b>0?l=t.__k[o]=q(l.type,l.props,l.key,l.ref?l.ref:null,l.__v):t.__k[o]=l,f=o+p,l.__=t,l.__b=t.__b+1,a=null,(i=l.__i=Ke(l,n,f,r))!=-1&&(r--,(a=n[i])&&(a.__u|=2)),a==null||a.__v==null?(i==-1&&(s>u?p--:s<u&&p++),typeof l.type!="function"&&(l.__u|=4)):i!=f&&(i==f-1?p--:i==f+1?p++:(i>f?p--:p++,l.__u|=4))):t.__k[o]=null;if(r)for(o=0;o<u;o++)(a=n[o])!=null&&(2&a.__u)==0&&(a.__e==_&&(_=F(a)),Ne(a,a));return _}function we(t,e,n,_){var s,o;if(typeof t.type=="function"){for(s=t.__k,o=0;s&&o<s.length;o++)s[o]&&(s[o].__=t,e=we(s[o],e,n,_));return e}t.__e!=e&&(_&&(e&&t.type&&!e.parentNode&&(e=F(t)),n.insertBefore(t.__e,e||null)),e=t.__e);do e=e&&e.nextSibling;while(e!=null&&e.nodeType==8);return e}function Ke(t,e,n,_){var s,o,l,a=t.key,f=t.type,i=e[n],u=i!=null&&(2&i.__u)==0;if(i===null&&a==null||u&&a==i.key&&f==i.type)return n;if(_>(u?1:0)){for(s=n-1,o=n+1;s>=0||o<e.length;)if((i=e[l=s>=0?s--:o++])!=null&&(2&i.__u)==0&&a==i.key&&f==i.type)return l}return-1}function de(t,e,n){e[0]=="-"?t.setProperty(e,n==null?"":n):t[e]=n==null?"":typeof n!="number"||Ge.test(e)?n:n+"px"}function V(t,e,n,_,s){var o,l;e:if(e=="style")if(typeof n=="string")t.style.cssText=n;else{if(typeof _=="string"&&(t.style.cssText=_=""),_)for(e in _)n&&e in n||de(t.style,e,"");if(n)for(e in n)_&&n[e]==_[e]||de(t.style,e,n[e])}else if(e[0]=="o"&&e[1]=="n")o=e!=(e=e.replace(ye,"$1")),l=e.toLowerCase(),e=l in t||e=="onFocusOut"||e=="onFocusIn"?l.slice(2):e.slice(2),t.l||(t.l={}),t.l[e+o]=n,n?_?n.u=_.u:(n.u=oe,t.addEventListener(e,o?ne:te,o)):t.removeEventListener(e,o?ne:te,o);else{if(s=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in t)try{t[e]=n==null?"":n;break e}catch(a){}typeof n=="function"||(n==null||n===!1&&e[4]!="-"?t.removeAttribute(e):t.setAttribute(e,e=="popover"&&n==1?"":n))}}function me(t){return function(e){if(this.l){var n=this.l[e.type+t];if(e.t==null)e.t=oe++;else if(e.t<n.u)return;return n(g.event?g.event(e):e)}}}function se(t,e,n,_,s,o,l,a,f,i){var u,r,p,c,m,y,b,v,h,C,N,A,D,j,ee,S=e.type;if(e.constructor!==void 0)return null;128&n.__u&&(f=!!(32&n.__u),o=[a=e.__e=n.__e]),(u=g.__b)&&u(e);e:if(typeof S=="function")try{if(v=e.props,h="prototype"in S&&S.prototype.render,C=(u=S.contextType)&&_[u.__c],N=u?C?C.props.value:u.__:_,n.__c?b=(r=e.__c=n.__c).__=r.__E:(h?e.__c=r=new S(v,N):(e.__c=r=new z(v,N),r.constructor=S,r.render=et),C&&C.sub(r),r.state||(r.state={}),r.__n=_,p=r.__d=!0,r.__h=[],r._sb=[]),h&&r.__s==null&&(r.__s=r.state),h&&S.getDerivedStateFromProps!=null&&(r.__s==r.state&&(r.__s=E({},r.__s)),E(r.__s,S.getDerivedStateFromProps(v,r.__s))),c=r.props,m=r.state,r.__v=e,p)h&&S.getDerivedStateFromProps==null&&r.componentWillMount!=null&&r.componentWillMount(),h&&r.componentDidMount!=null&&r.__h.push(r.componentDidMount);else{if(h&&S.getDerivedStateFromProps==null&&v!==c&&r.componentWillReceiveProps!=null&&r.componentWillReceiveProps(v,N),e.__v==n.__v||!r.__e&&r.shouldComponentUpdate!=null&&r.shouldComponentUpdate(v,r.__s,N)===!1){e.__v!=n.__v&&(r.props=v,r.state=r.__s,r.__d=!1),e.__e=n.__e,e.__k=n.__k,e.__k.some(function(L){L&&(L.__=e)}),G.push.apply(r.__h,r._sb),r._sb=[],r.__h.length&&l.push(r);break e}r.componentWillUpdate!=null&&r.componentWillUpdate(v,r.__s,N),h&&r.componentDidUpdate!=null&&r.__h.push(function(){r.componentDidUpdate(c,m,y)})}if(r.context=N,r.props=v,r.__P=t,r.__e=!1,A=g.__r,D=0,h)r.state=r.__s,r.__d=!1,A&&A(e),u=r.render(r.props,r.state,r.context),G.push.apply(r.__h,r._sb),r._sb=[];else do r.__d=!1,A&&A(e),u=r.render(r.props,r.state,r.context),r.state=r.__s;while(r.__d&&++D<25);r.state=r.__s,r.getChildContext!=null&&(_=E(E({},_),r.getChildContext())),h&&!p&&r.getSnapshotBeforeUpdate!=null&&(y=r.getSnapshotBeforeUpdate(c,m)),j=u!=null&&u.type===H&&u.key==null?Ce(u.props.children):u,a=xe(t,J(j)?j:[j],e,n,_,s,o,l,a,f,i),r.base=e.__e,e.__u&=-161,r.__h.length&&l.push(r),b&&(r.__E=r.__=null)}catch(L){if(e.__v=null,f||o!=null)if(L.then){for(e.__u|=f?160:128;a&&a.nodeType==8&&a.nextSibling;)a=a.nextSibling;o[o.indexOf(a)]=null,e.__e=a}else{for(ee=o.length;ee--;)_e(o[ee]);re(e)}else e.__e=n.__e,e.__k=n.__k,L.then||re(e);g.__e(L,e,n)}else o==null&&e.__v==n.__v?(e.__k=n.__k,e.__e=n.__e):a=e.__e=Ze(n.__e,e,n,_,s,o,l,f,i);return(u=g.diffed)&&u(e),128&e.__u?void 0:a}function re(t){t&&(t.__c&&(t.__c.__e=!0),t.__k&&t.__k.some(re))}function ke(t,e,n){for(var _=0;_<n.length;_++)le(n[_],n[++_],n[++_]);g.__c&&g.__c(e,t),t.some(function(s){try{t=s.__h,s.__h=[],t.some(function(o){o.call(s)})}catch(o){g.__e(o,s.__v)}})}function Ce(t){return typeof t!="object"||t==null||t.__b>0?t:J(t)?t.map(Ce):E({},t)}function Ze(t,e,n,_,s,o,l,a,f){var i,u,r,p,c,m,y,b=n.props||Q,v=e.props,h=e.type;if(h=="svg"?s="http://www.w3.org/2000/svg":h=="math"?s="http://www.w3.org/1998/Math/MathML":s||(s="http://www.w3.org/1999/xhtml"),o!=null){for(i=0;i<o.length;i++)if((c=o[i])&&"setAttribute"in c==!!h&&(h?c.localName==h:c.nodeType==3)){t=c,o[i]=null;break}}if(t==null){if(h==null)return document.createTextNode(v);t=document.createElementNS(s,h,v.is&&v),a&&(g.__m&&g.__m(e,o),a=!1),o=null}if(h==null)b===v||a&&t.data==v||(t.data=v);else{if(o=o&&Y.call(t.childNodes),!a&&o!=null)for(b={},i=0;i<t.attributes.length;i++)b[(c=t.attributes[i]).name]=c.value;for(i in b)c=b[i],i=="dangerouslySetInnerHTML"?r=c:i=="children"||i in v||i=="value"&&"defaultValue"in v||i=="checked"&&"defaultChecked"in v||V(t,i,null,c,s);for(i in v)c=v[i],i=="children"?p=c:i=="dangerouslySetInnerHTML"?u=c:i=="value"?m=c:i=="checked"?y=c:a&&typeof c!="function"||b[i]===c||V(t,i,c,b[i],s);if(u)a||r&&(u.__html==r.__html||u.__html==t.innerHTML)||(t.innerHTML=u.__html),e.__k=[];else if(r&&(t.innerHTML=""),xe(e.type=="template"?t.content:t,J(p)?p:[p],e,n,_,h=="foreignObject"?"http://www.w3.org/1999/xhtml":s,o,l,o?o[0]:n.__k&&F(n,0),a,f),o!=null)for(i=o.length;i--;)_e(o[i]);a||(i="value",h=="progress"&&m==null?t.removeAttribute("value"):m!=null&&(m!==t[i]||h=="progress"&&!m||h=="option"&&m!=b[i])&&V(t,i,m,b[i],s),i="checked",y!=null&&y!=t[i]&&V(t,i,y,b[i],s))}return t}function le(t,e,n){try{if(typeof t=="function"){var _=typeof t.__u=="function";_&&t.__u(),_&&e==null||(t.__u=t(e))}else t.current=e}catch(s){g.__e(s,n)}}function Ne(t,e,n){var _,s;if(g.unmount&&g.unmount(t),(_=t.ref)&&(_.current&&_.current!=t.__e||le(_,null,e)),(_=t.__c)!=null){if(_.componentWillUnmount)try{_.componentWillUnmount()}catch(o){g.__e(o,e)}_.base=_.__P=null}if(_=t.__k)for(s=0;s<_.length;s++)_[s]&&Ne(_[s],e,n||typeof t.type!="function");n||_e(t.__e),t.__c=t.__=t.__e=void 0}function et(t,e,n){return this.constructor(t,n)}function Se(t,e,n){var _,s,o,l;e==document&&(e=document.documentElement),g.__&&g.__(t,e),s=(_=typeof n=="function")?null:n&&n.__k||e.__k,o=[],l=[],se(e,t=(!_&&n||e).__k=Xe(H,null,[t]),s||Q,Q,e.namespaceURI,!_&&n?[n]:s?null:e.firstChild?Y.call(e.childNodes):null,o,!_&&n?n:s?s.__e:e.firstChild,_,l),ke(o,t,l)}Y=G.slice,g={__e:function(t,e,n,_){for(var s,o,l;e=e.__;)if((s=e.__c)&&!s.__)try{if((o=s.constructor)&&o.getDerivedStateFromError!=null&&(s.setState(o.getDerivedStateFromError(t)),l=s.__d),s.componentDidCatch!=null&&(s.componentDidCatch(t,_||{}),l=s.__d),l)return s.__E=s}catch(a){t=a}throw t}},he=0,ze=function(t){return t!=null&&t.constructor===void 0},z.prototype.setState=function(t,e){var n;n=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=E({},this.state),typeof t=="function"&&(t=t(E({},n),this.props)),t&&E(n,t),t!=null&&this.__v&&(e&&this._sb.push(e),pe(this))},z.prototype.forceUpdate=function(t){this.__v&&(this.__e=!0,t&&this.__h.push(t),pe(this))},z.prototype.render=H,U=[],ve=typeof Promise=="function"?Promise.prototype.then.bind(Promise.resolve()):setTimeout,ge=function(t,e){return t.__v.__b-e.__v.__b},X.__r=0,ye=/(PointerCapture)$|Capture$/i,oe=0,te=me(!1),ne=me(!0),Qe=0;var I,x,ie,Me,Z=0,Fe=[],w=g,Ae=w.__b,Ee=w.__r,Te=w.diffed,Pe=w.__c,Ue=w.unmount,De=w.__;function ue(t,e){w.__h&&w.__h(x,t,Z||e),Z=0;var n=x.__H||(x.__H={__:[],__h:[]});return t>=n.__.length&&n.__.push({}),n.__[t]}function B(t){return Z=1,tt(Re,t)}function tt(t,e,n){var _=ue(I++,2);if(_.t=t,!_.__c&&(_.__=[n?n(e):Re(void 0,e),function(a){var f=_.__N?_.__N[0]:_.__[0],i=_.t(f,a);f!==i&&(_.__N=[i,_.__[1]],_.__c.setState({}))}],_.__c=x,!x.__f)){var s=function(a,f,i){if(!_.__c.__H)return!0;var u=_.__c.__H.__.filter(function(p){return p.__c});if(u.every(function(p){return!p.__N}))return!o||o.call(this,a,f,i);var r=_.__c.props!==a;return u.some(function(p){if(p.__N){var c=p.__[0];p.__=p.__N,p.__N=void 0,c!==p.__[0]&&(r=!0)}}),o&&o.call(this,a,f,i)||r};x.__f=!0;var o=x.shouldComponentUpdate,l=x.componentWillUpdate;x.componentWillUpdate=function(a,f,i){if(this.__e){var u=o;o=void 0,s(a,f,i),o=u}l&&l.call(this,a,f,i)},x.shouldComponentUpdate=s}return _.__N||_.__}function R(t,e){var n=ue(I++,3);!w.__s&&He(n.__H,e)&&(n.__=t,n.u=e,x.__H.__h.push(n))}function O(t){return Z=5,k(function(){return{current:t}},[])}function k(t,e){var n=ue(I++,7);return He(n.__H,e)&&(n.__=t(),n.__H=e,n.__h=t),n.__}function nt(){for(var t;t=Fe.shift();){var e=t.__H;if(t.__P&&e)try{e.__h.some(K),e.__h.some(ae),e.__h=[]}catch(n){e.__h=[],w.__e(n,t.__v)}}}w.__b=function(t){x=null,Ae&&Ae(t)},w.__=function(t,e){t&&e.__k&&e.__k.__m&&(t.__m=e.__k.__m),De&&De(t,e)},w.__r=function(t){Ee&&Ee(t),I=0;var e=(x=t.__c).__H;e&&(ie===x?(e.__h=[],x.__h=[],e.__.some(function(n){n.__N&&(n.__=n.__N),n.u=n.__N=void 0})):(e.__h.some(K),e.__h.some(ae),e.__h=[],I=0)),ie=x},w.diffed=function(t){Te&&Te(t);var e=t.__c;e&&e.__H&&(e.__H.__h.length&&(Fe.push(e)!==1&&Me===w.requestAnimationFrame||((Me=w.requestAnimationFrame)||rt)(nt)),e.__H.__.some(function(n){n.u&&(n.__H=n.u),n.u=void 0})),ie=x=null},w.__c=function(t,e){e.some(function(n){try{n.__h.some(K),n.__h=n.__h.filter(function(_){return!_.__||ae(_)})}catch(_){e.some(function(s){s.__h&&(s.__h=[])}),e=[],w.__e(_,n.__v)}}),Pe&&Pe(t,e)},w.unmount=function(t){Ue&&Ue(t);var e,n=t.__c;n&&n.__H&&(n.__H.__.some(function(_){try{K(_)}catch(s){e=s}}),n.__H=void 0,e&&w.__e(e,n.__v))};var Le=typeof requestAnimationFrame=="function";function rt(t){var e,n=function(){clearTimeout(_),Le&&cancelAnimationFrame(e),setTimeout(t)},_=setTimeout(n,35);Le&&(e=requestAnimationFrame(n))}function K(t){var e=x,n=t.__c;typeof n=="function"&&(t.__c=void 0,n()),x=e}function ae(t){var e=x;t.__c=t.__(),x=e}function He(t,e){return!t||t.length!==e.length||e.some(function(n,_){return n!==t[_]})}function Re(t,e){return typeof e=="function"?e(t):e}var T=window==null?void 0:window.RAW_DATASET,P=["author","days_bucket","lang","clusterPath","repoName","count"],M=T[0].length-1,W=Array(M).fill(-1).map((t,e)=>e),ot=W.map(t=>{var n;return typeof((n=T==null?void 0:T[0])==null?void 0:n[t])=="number"?(_,s)=>(_||0)-(s||0):(_,s)=>String(_).localeCompare(String(s))}),ce=W.map(t=>_t(T,t).sort(ot[t]));function _t(t,e){let n=new Set(t.map(_=>_[e]));return Array.from(n)}var Oe=P.indexOf("clusterPath"),Ie=P.indexOf("repoName");var st=0,dt=Array.isArray;function d(t,e,n,_,s,o){e||(e={});var l,a,f=e;if("ref"in f)for(a in f={},e)a=="ref"?l=e[a]:f[a]=e[a];var i={type:t,props:f,key:n,ref:l,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--st,__i:-1,__u:0,__source:s,__self:o};if(typeof t=="function"&&(l=t.defaultProps))for(a in l)f[a]===void 0&&(f[a]=l[a]);return g.vnode&&g.vnode(i),i}function Be({label:t,values:e,selectedSet:n,onChange:_}){let[s,o]=B(!1),[l,a]=B(""),f=O(null);R(()=>{function c(m){f.current&&!f.current.contains(m.target)&&o(!1)}return document.addEventListener("mousedown",c),()=>document.removeEventListener("mousedown",c)},[]);let i=k(()=>{if(!l)return e;let c=l.toLowerCase();return e.filter(m=>String(m).toLowerCase().includes(c))},[e,l]),u=n.size,r=e.length,p=c=>{let m=new Set(n);m.has(c)?m.delete(c):m.add(c),_(m)};return d("div",{ref:f,className:"relative",children:[d("label",{className:"block font-semibold mb-1.5",children:[t," ",d("span",{className:"text-gray-600 font-normal",children:["(",u,"/",r,")"]})]}),d("button",{type:"button",onClick:()=>o(!s),className:"w-full px-3 py-2 text-left bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",children:[d("span",{className:"text-gray-700",children:u===0?"Select...":u===r?"All selected":`${u} selected`}),d("span",{className:"float-right",children:"\\u25BC"})]}),s&&d("div",{className:"absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-80 overflow-hidden",children:[d("div",{className:"p-2 border-b border-gray-200",children:[d("input",{type:"text",placeholder:"Search...",value:l,onChange:c=>a(c.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500",onClick:c=>c.stopPropagation()}),d("div",{className:"flex gap-2 mt-2",children:[d("button",{type:"button",onClick:()=>_(new Set(e.map(String))),className:"flex-1 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500",children:"Select All"}),d("button",{type:"button",onClick:()=>_(new Set),className:"flex-1 px-3 py-1.5 text-sm bg-gray-600 text-white rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500",children:"Unselect All"})]})]}),d("div",{className:"overflow-y-auto max-h-64",children:i.length===0?d("div",{className:"px-3 py-2 text-gray-500 text-center",children:"No matches found"}):i.map(c=>{let m=String(c),y=n.has(m);return d("label",{className:"flex items-center px-3 py-2 hover:bg-gray-100 cursor-pointer",children:[d("input",{type:"checkbox",checked:y,onChange:()=>p(m),className:"mr-2 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"}),d("span",{className:"text-gray-900",children:m})]},m)})})]})]})}function $({data:t,layout:e,config:n}){let _=O(null);return R(()=>{let s=_.current;s&&Plotly.newPlot(s,t,e,n)},[t,e,n]),d("div",{ref:_,className:"w-full"})}function We({columnName:t,columnIdx:e,keys:n,values:_,totals:s,contributions:o}){let{labels:l,hoverText:a}=k(()=>{let r=[],p=[];return n.forEach(c=>{let m=s.get(c)||0,y=o.get(c),b=[];for(let h=0;h<M;h++){if(h===e)continue;let C=y==null?void 0:y.get(h);if(C){let N=Array.from(C.entries()).sort((A,D)=>D[1]-A[1]).slice(0,3).map(([A,D])=>`${A}(${(D/m*100).toFixed(1)}%)`).join("<br>-");N&&b.push(`${P[h]}<br>-`+N)}}let v=`${String(c)} (${m})`;r.push(v),p.push(b.length>0?`${v}<br>${b.join("<br>")}`:v)}),{labels:r,hoverText:p}},[t,e,n,s,o]),f=k(()=>[{type:"sunburst",labels:l,parents:l.map(()=>""),values:_,hovertext:a,hovertemplate:"%{hovertext}<extra></extra>",branchvalues:"total"}],[l,a,_]),i=k(()=>({margin:{l:0,r:0,t:10,b:10},sunburstcolorway:["#4F46E5","#10B981","#F59E0B","#EF4444","#06B6D4","#8B5CF6","#F43F5E"],extendsunburstcolors:!0,height:300}),[]),u=k(()=>({responsive:!0,displayModeBar:!1}),[]);return d("div",{className:"bg-white border border-gray-200 rounded-lg p-4 shadow-sm",children:[d("h3",{className:"text-lg font-semibold mb-3 text-gray-800",children:[t," - Total"]}),l.length?d($,{data:f,layout:i,config:u}):d("div",{className:"text-gray-500 py-6 text-center",children:"No data to display"})]})}function $e({dataset:t}){let e=O(null),n=k(()=>lt(t),[t]);return R(()=>{let _=e.current;if(!_)return;if(!n.ids.length){_.innerHTML=\'<div class="text-gray-500 py-6 text-center">No path data to display</div>\';return}let s={type:"sunburst",ids:n.ids,labels:n.labels,parents:n.parents,values:n.values,branchvalues:"total",maxdepth:3},o={margin:{l:0,r:0,t:10,b:10},sunburstcolorway:["#4F46E5","#10B981","#F59E0B","#EF4444","#06B6D4","#8B5CF6","#F43F5E"],extendsunburstcolors:!0,height:400},l={responsive:!0,displayModeBar:!1};Plotly.newPlot(_,[s],o,l)},[n]),d("div",{ref:e,className:"w-full"})}function lt(t){let e=t,n=new Map,_=new Set;for(let u of e){let r=(u[Ie]+"/"+u[Oe]).trim(),p=Number(u[M])||0;if(!r)continue;let c=r.split("/").filter(y=>y&&y!==".");if(c.length===0)continue;let m=c.join("/");n.set(m,(n.get(m)||0)+p);for(let y=0;y<c.length;y++){let b=c.slice(0,y+1).join("/");_.add(b)}}if(_.size===0)return{ids:[],labels:[],parents:[],values:[]};let s=new Map(Array.from(_,u=>[u,0]));for(let[u,r]of n){let p=u.split("/");for(let c=0;c<p.length;c++){let m=p.slice(0,c+1).join("/");s.set(m,(s.get(m)||0)+r)}}let o=[],l=[],a=[],f=[],i=Array.from(_);i.sort((u,r)=>{let p=u.split("/").length,c=r.split("/").length;return p!==c?p-c:u.localeCompare(r)});for(let u of i){let r=u.split("/"),p=r[r.length-1]||u,c=r.length>1?r.slice(0,-1).join("/"):"";o.push(u),l.push(p),a.push(c),f.push(s.get(u)||0)}return{ids:o,labels:l,parents:a,values:f}}function Ve(){let{initialFilters:t,valueOptions:e}=k(()=>{let l={},a={};for(let f=0;f<M;f++)l[f]=new Set(ce[f].map(i=>String(i))),a[f]=ce[f];return{initialFilters:l,valueOptions:a}},[]),[n,_]=B(t),s=k(()=>T.filter(l=>je(l,n)),[n]),o=k(()=>{let l=new Map,a=P.indexOf("days_bucket"),f=null,i=null;s.forEach(u=>{let r=u[a],p=Number(u[M])||0;l.set(r,(l.get(r)||0)+p),console.error("ADD",r),(!f||r<f)&&(f=r),(!i||r>i)&&(i=r)});for(let u=f;u<i;u++)u%10>0&&u%10<=4&&!l.has(u)&&(l.set(u,0),console.error("ADD",u));return Array.from(l.entries()).sort((u,r)=>u[0]-r[0])},[s]);return d("div",{className:"max-w-4xl mx-auto bg-white rounded-lg shadow-sm p-4",children:[d("h1",{className:"border-gray-300 text-xl",children:"Git Contribution Statistics"}),d("div",{id:"filters",className:"grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3",children:W.map((l,a)=>d(Be,{label:P[a],values:e[a],selectedSet:n[a],onChange:f=>_(i=>({...i,[a]:f}))},a))}),d("div",{children:d($,{data:[{x:o.map(l=>l[0]),y:o.map(l=>l[1]),type:"scatter",line:{shape:"spline",smoothing:1.3}}],layout:{xaxis:{visible:!1,type:"category"},yaxis:{visible:!1},height:150,margin:{l:0,r:0,t:0,b:0,pad:0},bargap:0,bargroupgap:0,selectdirection:"h",zoomdirection:"x"},config:{displayModeBar:!1,displaylogo:!1}})}),d("div",{children:[d("h2",{className:"border-b border-gray-300",children:"Column Totals"}),d("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4 mt-4",children:W.map((l,a)=>{let f=T.filter(c=>je(c,n)),{keys:i,values:u,totals:r,contributions:p}=it(f,a);return d(We,{columnName:P[a],columnIdx:a,keys:i,values:u,totals:r,contributions:p},a)})})]}),d("div",{className:"mt-8",children:[d("h2",{className:"border-b border-gray-300 pb-2.5",children:"Repository Paths Sunburst"}),d("p",{className:"text-sm text-gray-600 mt-2 mb-3",children:["Breakdown by folder structure based on ",d("code",{children:"clusterPath"})," within current filters."]}),d($e,{dataset:s})]})]})}function je(t,e){for(let n=0;n<M;n++){let _=e[n];if(_&&!_.has(String(t[n])))return!1}return!0}function it(t,e){let n=new Map,_=new Map;for(let o of t){let l=o[e],a=Number(o[o.length-1])||0;n.set(l,(n.get(l)||0)+a),_.has(l)||_.set(l,new Map);let f=_.get(l);for(let i=0;i<M;i++){if(i===e)continue;f.has(i)||f.set(i,new Map);let u=f.get(i),r=o[i];u.set(r,(u.get(r)||0)+a)}}let s=Array.from(n.entries()).sort((o,l)=>l[1]-o[1]);return{keys:s.map(([o])=>o),values:s.map(([,o])=>o),totals:n,contributions:_}}var qe=document.getElementById("root");qe&&Se(d(Ve,{}),qe);})();</script>\n</body>\n</html>\nw';
|
|
49
|
+
|
|
50
|
+
// src/output/report_template.ts
|
|
51
|
+
function generateHtmlReport(data, outputFile) {
|
|
52
|
+
const finalOutputPath = path.join(outputFile);
|
|
53
|
+
let htmlContent = dist_default.split("__DATASET_JSON__");
|
|
54
|
+
fs.writeFileSync(finalOutputPath, htmlContent[0]);
|
|
55
|
+
fs.appendFileSync(finalOutputPath, "\n[\n");
|
|
56
|
+
for (let i = 0; i < data.length; i++) {
|
|
57
|
+
fs.appendFileSync(finalOutputPath, JSON.stringify(data[i]) + ",\n");
|
|
58
|
+
}
|
|
59
|
+
fs.appendFileSync(finalOutputPath, "\n]\n");
|
|
60
|
+
fs.appendFileSync(finalOutputPath, htmlContent[1]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/git.ts
|
|
64
|
+
var import_path = __toESM(require("path"));
|
|
65
|
+
var import_fs = __toESM(require("fs"));
|
|
38
66
|
|
|
39
67
|
// src/util/exec.ts
|
|
40
68
|
var import_child_process = require("child_process");
|
|
41
69
|
function execAsync(command, args = [], options = {}) {
|
|
42
70
|
return new Promise((resolve2, reject) => {
|
|
43
|
-
const child = (0, import_child_process.spawn)(command, args, { ...options
|
|
71
|
+
const child = (0, import_child_process.spawn)(command, args, { ...options });
|
|
44
72
|
let stdout = [];
|
|
45
|
-
let
|
|
73
|
+
let stderr2 = [];
|
|
46
74
|
let stdoutBuffer = "";
|
|
47
75
|
let stderrBuffer = "";
|
|
48
76
|
child.stdout.on("data", (data) => {
|
|
@@ -57,7 +85,7 @@ function execAsync(command, args = [], options = {}) {
|
|
|
57
85
|
stderrBuffer += data.toString();
|
|
58
86
|
const lines = stderrBuffer.split("\n");
|
|
59
87
|
for (let i = 0; i < lines.length - 1; i++) {
|
|
60
|
-
|
|
88
|
+
stderr2.push(lines[i]);
|
|
61
89
|
}
|
|
62
90
|
stderrBuffer = lines[lines.length - 1];
|
|
63
91
|
});
|
|
@@ -69,282 +97,19 @@ function execAsync(command, args = [], options = {}) {
|
|
|
69
97
|
stdout.push(stdoutBuffer);
|
|
70
98
|
}
|
|
71
99
|
if (stderrBuffer.length > 0) {
|
|
72
|
-
|
|
100
|
+
stderr2.push(stderrBuffer);
|
|
73
101
|
}
|
|
74
102
|
if (code === 0) {
|
|
75
|
-
resolve2({ stdout, stderr });
|
|
103
|
+
resolve2({ stdout, stderr: stderr2 });
|
|
76
104
|
} else {
|
|
77
|
-
reject(new Error(`Command failed with code ${code}
|
|
78
|
-
${
|
|
105
|
+
reject(new Error(`Command ${command} ${JSON.stringify(args)} failed with code ${code}
|
|
106
|
+
${stderr2.join("\n")}`));
|
|
79
107
|
}
|
|
80
108
|
});
|
|
81
109
|
});
|
|
82
110
|
}
|
|
83
111
|
|
|
84
|
-
// src/index.ts
|
|
85
|
-
var fs4 = __toESM(require("fs"));
|
|
86
|
-
var path4 = __toESM(require("path"));
|
|
87
|
-
|
|
88
|
-
// src/output/report_template.ts
|
|
89
|
-
var path = __toESM(require("path"));
|
|
90
|
-
var fs = __toESM(require("fs"));
|
|
91
|
-
|
|
92
|
-
// src/output/report_template.html
|
|
93
|
-
var report_template_default = `<!DOCTYPE html>
|
|
94
|
-
<!--suppress TypeScriptMissingConfigOption -->
|
|
95
|
-
<html lang="en">
|
|
96
|
-
<head>
|
|
97
|
-
<meta charset="UTF-8">
|
|
98
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
99
|
-
<title>Git Blame Statistics</title>
|
|
100
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
101
|
-
<script src="https://cdn.plot.ly/plotly-3.3.0.min.js" charset="utf-8"></script>
|
|
102
|
-
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
103
|
-
<!-- React 18 UMD builds -->
|
|
104
|
-
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|
105
|
-
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|
106
|
-
<!-- Babel Standalone for in-browser JSX transform -->
|
|
107
|
-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
108
|
-
</head>
|
|
109
|
-
<body class="font-sans m-0 bg-gray-50 text-gray-900">
|
|
110
|
-
<div id="root"></div>
|
|
111
|
-
<script type="text/babel" data-presets="react">
|
|
112
|
-
const RAW_DATASET =
|
|
113
|
-
__DATASET_JSON__
|
|
114
|
-
|| [];
|
|
115
|
-
// Fixed schema as per pipeline: [author, days_bucket, lang, clusterPath, repoName, count]
|
|
116
|
-
const HEADER_LABELS = ["author", "days_bucket", "lang", "clusterPath", "repoName", "count"];
|
|
117
|
-
// -------- Client-side grouping/filtering engine --------
|
|
118
|
-
const KEY_INDEX = Object.fromEntries(HEADER_LABELS.map((k, i) => [k, i]));
|
|
119
|
-
const COUNT_IDX_FROM_END = 1; // last element is count
|
|
120
|
-
|
|
121
|
-
const allKeys = HEADER_LABELS.filter(k => k !== 'count');
|
|
122
|
-
const TOP_N = 20;
|
|
123
|
-
const BUCKET_COLORS = [
|
|
124
|
-
'rgba(214, 40, 40, 0.7)',
|
|
125
|
-
'rgba(247, 127, 0, 0.7)',
|
|
126
|
-
'rgba(252, 191, 73, 0.7)',
|
|
127
|
-
'rgba(168, 218, 142, 0.7)',
|
|
128
|
-
'rgba(75, 192, 192, 0.7)',
|
|
129
|
-
'rgba(54, 162, 235, 0.7)',
|
|
130
|
-
'rgba(153, 102, 255, 0.7)',
|
|
131
|
-
'rgba(201, 203, 207, 0.7)'
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
function uniqueValues(arr, idx) {
|
|
135
|
-
const set = new Set(arr.map(r => r[idx]));
|
|
136
|
-
return Array.from(set);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function pivot(
|
|
140
|
-
dataset,
|
|
141
|
-
primaryKeyIndex,
|
|
142
|
-
secondaryKeyIndex,
|
|
143
|
-
filterState
|
|
144
|
-
) {
|
|
145
|
-
const pivot = new Map();
|
|
146
|
-
const totals = new Map();
|
|
147
|
-
const secValuesSet = new Set();
|
|
148
|
-
for (const row of dataset) {
|
|
149
|
-
let ok = true;
|
|
150
|
-
for (const key of allKeys) {
|
|
151
|
-
const idx = KEY_INDEX[key];
|
|
152
|
-
const sel = filterState[key];
|
|
153
|
-
if (sel && !sel.has(String(row[idx]))) {
|
|
154
|
-
ok = false;
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (!ok) continue;
|
|
159
|
-
const pk = row[primaryKeyIndex];
|
|
160
|
-
const sk = row[secondaryKeyIndex];
|
|
161
|
-
const count = Number(row[row.length - COUNT_IDX_FROM_END]) || 0;
|
|
162
|
-
if (!pivot.has(pk)) pivot.set(pk, new Map());
|
|
163
|
-
pivot.get(pk).set(sk, (pivot.get(pk).get(sk) || 0) + count);
|
|
164
|
-
totals.set(pk, (totals.get(pk) || 0) + count);
|
|
165
|
-
secValuesSet.add(sk);
|
|
166
|
-
}
|
|
167
|
-
const primaryKeys = Array.from(pivot.keys()).sort((a, b) => (totals.get(b) || 0) - (totals.get(a) || 0));
|
|
168
|
-
const secondaryKeys = Array.from(secValuesSet).sort((a, b) => String(a).localeCompare(String(b)));
|
|
169
|
-
return {pivot, totals, primaryKeys, secondaryKeys};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ---------- React Components ----------
|
|
173
|
-
function ChartView({labels, datasets}) {
|
|
174
|
-
const canvasRef = React.useRef(null);
|
|
175
|
-
const chartRef = React.useRef(null);
|
|
176
|
-
React.useEffect(() => {
|
|
177
|
-
const ctx = canvasRef.current.getContext('2d');
|
|
178
|
-
chartRef.current = new Chart(ctx, {
|
|
179
|
-
type: 'bar',
|
|
180
|
-
data: {labels: [], datasets: []},
|
|
181
|
-
options: {
|
|
182
|
-
indexAxis: 'y',
|
|
183
|
-
scales: {
|
|
184
|
-
x: {stacked: true, beginAtZero: true},
|
|
185
|
-
y: {stacked: true, ticks: {autoSkip: false}}
|
|
186
|
-
},
|
|
187
|
-
plugins: {
|
|
188
|
-
tooltip: {
|
|
189
|
-
callbacks: {
|
|
190
|
-
label: function (context) {
|
|
191
|
-
let label = context.dataset.label || '';
|
|
192
|
-
if (label) label += ': ';
|
|
193
|
-
if (context.parsed.x !== null) label += context.parsed.x.toLocaleString();
|
|
194
|
-
return label;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
return () => {
|
|
202
|
-
chartRef.current && chartRef.current.destroy();
|
|
203
|
-
};
|
|
204
|
-
}, []);
|
|
205
|
-
|
|
206
|
-
React.useEffect(() => {
|
|
207
|
-
if (!chartRef.current) return;
|
|
208
|
-
chartRef.current.data.labels = labels;
|
|
209
|
-
chartRef.current.data.datasets = datasets;
|
|
210
|
-
chartRef.current.update();
|
|
211
|
-
}, [labels, datasets]);
|
|
212
|
-
|
|
213
|
-
return <canvas ref={canvasRef}/>;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function MultiSelect({label, values, selectedSet, onChange}) {
|
|
217
|
-
const selCount = selectedSet.size;
|
|
218
|
-
const total = values.length;
|
|
219
|
-
return (
|
|
220
|
-
<div>
|
|
221
|
-
<label className="block font-semibold mb-1.5">
|
|
222
|
-
{label}{' '}
|
|
223
|
-
<span className="text-gray-600 font-normal">({selCount}/{total})</span>
|
|
224
|
-
</label>
|
|
225
|
-
<select
|
|
226
|
-
multiple
|
|
227
|
-
size={Math.min(8, Math.max(3, values.length))}
|
|
228
|
-
className="w-full"
|
|
229
|
-
onChange={(e) => {
|
|
230
|
-
const chosen = Array.from(e.target.selectedOptions).map(o => o.value);
|
|
231
|
-
onChange(new Set(chosen));
|
|
232
|
-
}}
|
|
233
|
-
>
|
|
234
|
-
{values.map(v => {
|
|
235
|
-
const strVal = String(v);
|
|
236
|
-
return (
|
|
237
|
-
<option key={strVal} value={strVal} selected={selectedSet.has(strVal)}>
|
|
238
|
-
{strVal}
|
|
239
|
-
</option>
|
|
240
|
-
);
|
|
241
|
-
})}
|
|
242
|
-
</select>
|
|
243
|
-
</div>
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function App() {
|
|
248
|
-
const {initialFilters, valueOptions} = React.useMemo(() => {
|
|
249
|
-
const filters = {};
|
|
250
|
-
const options = {};
|
|
251
|
-
for (const key of allKeys) {
|
|
252
|
-
const idx = KEY_INDEX[key];
|
|
253
|
-
const values = uniqueValues(RAW_DATASET, idx).sort((a, b) => {
|
|
254
|
-
if (key === 'days_bucket') return Number(a) - Number(b);
|
|
255
|
-
return String(a).localeCompare(String(b));
|
|
256
|
-
});
|
|
257
|
-
filters[key] = new Set(values.map(v => String(v)));
|
|
258
|
-
options[key] = values;
|
|
259
|
-
}
|
|
260
|
-
return {initialFilters: filters, valueOptions: options};
|
|
261
|
-
}, []);
|
|
262
|
-
const [filters, setFilters] = React.useState(initialFilters);
|
|
263
|
-
const [primaryKeyIndex, setPrimaryKeyIndex] = React.useState(0);
|
|
264
|
-
const [secondaryKeyIndex, setSecondaryKeyIndex] = React.useState(1);
|
|
265
|
-
|
|
266
|
-
const {chartLabels, datasets} = React.useMemo(() => {
|
|
267
|
-
const {
|
|
268
|
-
pivot: pv,
|
|
269
|
-
primaryKeys,
|
|
270
|
-
secondaryKeys
|
|
271
|
-
} = pivot(RAW_DATASET, primaryKeyIndex, secondaryKeyIndex, filters);
|
|
272
|
-
const chartPrimaryKeys = primaryKeys.slice(0, TOP_N);
|
|
273
|
-
const ds = secondaryKeys.map((sk, i) => ({
|
|
274
|
-
label: String(sk),
|
|
275
|
-
data: chartPrimaryKeys.map(pk => (pv.get(pk)?.get(sk)) || 0),
|
|
276
|
-
backgroundColor: BUCKET_COLORS[i % BUCKET_COLORS.length]
|
|
277
|
-
}));
|
|
278
|
-
return {chartLabels: chartPrimaryKeys, datasets: ds};
|
|
279
|
-
}, [filters, primaryKeyIndex, secondaryKeyIndex]);
|
|
280
|
-
|
|
281
|
-
return (
|
|
282
|
-
<div className="max-w-4xl mx-auto my-5 p-5 bg-white rounded-lg shadow-sm">
|
|
283
|
-
<h1 className="border-b border-gray-300 pb-2.5">Git Contribution Statistics</h1>
|
|
284
|
-
<div className="controls">
|
|
285
|
-
<h2 className="border-b border-gray-300 pb-2.5">Controls</h2>
|
|
286
|
-
<div className="flex gap-4 flex-wrap items-center">
|
|
287
|
-
<label>
|
|
288
|
-
Primary group:
|
|
289
|
-
<select value={primaryKeyIndex} onChange={e => setPrimaryKeyIndex(Number(e.target.value))}>
|
|
290
|
-
{allKeys.map((__, i) => (
|
|
291
|
-
<option key={i} value={i}>{HEADER_LABELS[i]}</option>
|
|
292
|
-
))}
|
|
293
|
-
</select>
|
|
294
|
-
</label>
|
|
295
|
-
<label>
|
|
296
|
-
Secondary group:
|
|
297
|
-
<select value={secondaryKeyIndex}
|
|
298
|
-
onChange={e => setSecondaryKeyIndex(Number(e.target.value))}>
|
|
299
|
-
{allKeys.map((__, i) => (
|
|
300
|
-
<option key={i} value={i}>{HEADER_LABELS[i]}</option>
|
|
301
|
-
))}
|
|
302
|
-
</select>
|
|
303
|
-
</label>
|
|
304
|
-
</div>
|
|
305
|
-
<div id="filters" className="mt-3 grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-3">
|
|
306
|
-
{allKeys.map((key, idx) => (
|
|
307
|
-
<MultiSelect
|
|
308
|
-
key={idx}
|
|
309
|
-
label={HEADER_LABELS[idx]}
|
|
310
|
-
values={valueOptions[key]}
|
|
311
|
-
selectedSet={filters[key]}
|
|
312
|
-
onChange={(newSet) => setFilters(prev => ({...prev, [key]: newSet}))}
|
|
313
|
-
/>
|
|
314
|
-
))}
|
|
315
|
-
</div>
|
|
316
|
-
</div>
|
|
317
|
-
<div className="mt-5">
|
|
318
|
-
<h2 id="chartTitle" className="border-b border-gray-300 pb-2.5">Chart</h2>
|
|
319
|
-
<ChartView labels={chartLabels} datasets={datasets}/>
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
326
|
-
root.render(<App/>);
|
|
327
|
-
</script>
|
|
328
|
-
</body>
|
|
329
|
-
</html>
|
|
330
|
-
`;
|
|
331
|
-
|
|
332
|
-
// src/output/report_template.ts
|
|
333
|
-
function generateHtmlReport(data, outputFile) {
|
|
334
|
-
const finalOutputPath = path.join(outputFile);
|
|
335
|
-
let htmlContent = report_template_default.split("__DATASET_JSON__");
|
|
336
|
-
fs.writeFileSync(finalOutputPath, htmlContent[0]);
|
|
337
|
-
fs.appendFileSync(finalOutputPath, "\n[\n");
|
|
338
|
-
for (let i = 0; i < data.length; i++) {
|
|
339
|
-
fs.appendFileSync(finalOutputPath, JSON.stringify(data[i]) + ",\n");
|
|
340
|
-
}
|
|
341
|
-
fs.appendFileSync(finalOutputPath, "\n]\n");
|
|
342
|
-
fs.appendFileSync(finalOutputPath, htmlContent[1]);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
112
|
// src/git.ts
|
|
346
|
-
var import_path = __toESM(require("path"));
|
|
347
|
-
var import_fs = __toESM(require("fs"));
|
|
348
113
|
async function executeGitBlamePorcelain(file, repoRoot, revisionBoundary, since) {
|
|
349
114
|
const args = ["blame", "--line-porcelain"];
|
|
350
115
|
if (since) {
|
|
@@ -366,18 +131,18 @@ function parsePorcelain(blameOutput, fields) {
|
|
|
366
131
|
const commiterTimePos = fields.indexOf("committer-time");
|
|
367
132
|
const boundaryPos = fields.indexOf("boundary");
|
|
368
133
|
const commitPos = fields.indexOf("commit");
|
|
369
|
-
let emptyRow =
|
|
134
|
+
let emptyRow = {};
|
|
370
135
|
if (commiterTimePos >= 0) {
|
|
371
136
|
emptyRow[commiterTimePos] = 0;
|
|
372
137
|
}
|
|
373
138
|
if (boundaryPos >= 0) {
|
|
374
139
|
emptyRow[boundaryPos] = 0;
|
|
375
140
|
}
|
|
376
|
-
let nextRow =
|
|
141
|
+
let nextRow = { ...emptyRow };
|
|
377
142
|
const result = [];
|
|
378
143
|
for (const line of blameOutput) {
|
|
379
144
|
if (line.startsWith(" ")) {
|
|
380
|
-
result.push(
|
|
145
|
+
result.push({ ...nextRow });
|
|
381
146
|
continue;
|
|
382
147
|
}
|
|
383
148
|
if (commitPos >= 0) {
|
|
@@ -385,22 +150,22 @@ function parsePorcelain(blameOutput, fields) {
|
|
|
385
150
|
if (firstSpace === 40) {
|
|
386
151
|
const possibleHash = line.substring(0, firstSpace);
|
|
387
152
|
if (/^\^?[0-9a-f]{40}$/i.test(possibleHash)) {
|
|
388
|
-
nextRow =
|
|
389
|
-
nextRow
|
|
153
|
+
nextRow = { ...emptyRow };
|
|
154
|
+
nextRow.commit = possibleHash.replace(/^\^/, "");
|
|
390
155
|
continue;
|
|
391
156
|
}
|
|
392
157
|
}
|
|
393
158
|
}
|
|
394
159
|
if (userPos >= 0 && line.startsWith("author ")) {
|
|
395
|
-
nextRow
|
|
160
|
+
nextRow.author = line.substring("author ".length).replace(/^<|>$/g, "");
|
|
396
161
|
continue;
|
|
397
162
|
}
|
|
398
163
|
if (commiterTimePos >= 0 && line.startsWith("committer-time ")) {
|
|
399
|
-
nextRow
|
|
164
|
+
nextRow.time = parseInt(line.substring("committer-time ".length), 10);
|
|
400
165
|
continue;
|
|
401
166
|
}
|
|
402
167
|
if (boundaryPos >= 0 && line.startsWith("boundary")) {
|
|
403
|
-
nextRow
|
|
168
|
+
nextRow.boundary = 1;
|
|
404
169
|
}
|
|
405
170
|
}
|
|
406
171
|
return result;
|
|
@@ -425,37 +190,14 @@ async function findRevision(repoRoot, commitsBack) {
|
|
|
425
190
|
}
|
|
426
191
|
return void 0;
|
|
427
192
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
async read(filePath) {
|
|
437
|
-
return import_fs2.default.readFileSync(this.resolve(filePath), "utf8");
|
|
438
|
-
}
|
|
439
|
-
async write(filePath, content) {
|
|
440
|
-
let path1 = import_path2.default.resolve(this.resolve(filePath), "..");
|
|
441
|
-
import_fs2.default.mkdirSync(path1, { recursive: true });
|
|
442
|
-
import_fs2.default.writeFileSync(this.resolve(filePath), content);
|
|
443
|
-
}
|
|
444
|
-
async append(filePath, content) {
|
|
445
|
-
return new Promise((resolve2, reject) => {
|
|
446
|
-
import_fs2.default.appendFile(this.resolve(filePath), content, (err) => {
|
|
447
|
-
if (!err) {
|
|
448
|
-
resolve2(void 0);
|
|
449
|
-
} else {
|
|
450
|
-
reject(err);
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
resolve(filePath) {
|
|
456
|
-
return import_path2.default.normalize(`${this.basePath}/${filePath}`);
|
|
457
|
-
}
|
|
458
|
-
};
|
|
193
|
+
async function git_ls_files(repoRootPath, subdirPath) {
|
|
194
|
+
const { stdout: lsFilesOut } = await execAsync(
|
|
195
|
+
"git",
|
|
196
|
+
["ls-files", "--", subdirPath || "."],
|
|
197
|
+
{ cwd: repoRootPath }
|
|
198
|
+
);
|
|
199
|
+
return lsFilesOut.filter((line) => line && line.length > 0);
|
|
200
|
+
}
|
|
459
201
|
|
|
460
202
|
// src/util/AsyncGeneratorUtil.ts
|
|
461
203
|
var AsyncGeneratorUtil = class {
|
|
@@ -503,22 +245,38 @@ var AsyncGeneratorUtil = class {
|
|
|
503
245
|
});
|
|
504
246
|
}
|
|
505
247
|
};
|
|
506
|
-
var AsyncIteratorWrapperImpl = class
|
|
248
|
+
var AsyncIteratorWrapperImpl = class {
|
|
507
249
|
constructor(source) {
|
|
508
250
|
this.source = source;
|
|
509
251
|
}
|
|
252
|
+
chunked(size) {
|
|
253
|
+
return streamOf(this.__chunked(size));
|
|
254
|
+
}
|
|
255
|
+
async *__chunked(size) {
|
|
256
|
+
let chunk = [];
|
|
257
|
+
for await (const item of this.source) {
|
|
258
|
+
chunk.push(item);
|
|
259
|
+
if (chunk.length === size) {
|
|
260
|
+
yield chunk;
|
|
261
|
+
chunk = [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (chunk.length > 0) {
|
|
265
|
+
yield chunk;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
510
268
|
get() {
|
|
511
269
|
return this.source;
|
|
512
270
|
}
|
|
513
271
|
map(mapper) {
|
|
514
|
-
return
|
|
272
|
+
return streamOf(async function* (source, mapper2) {
|
|
515
273
|
for await (const item of source) {
|
|
516
274
|
yield mapper2(item);
|
|
517
275
|
}
|
|
518
276
|
}(this.source, mapper));
|
|
519
277
|
}
|
|
520
278
|
flatMap(mapper) {
|
|
521
|
-
return
|
|
279
|
+
return streamOf(AsyncGeneratorUtil.flatMap(this.source, mapper));
|
|
522
280
|
}
|
|
523
281
|
async forEach(consumer) {
|
|
524
282
|
for await (const item of this.source) {
|
|
@@ -526,217 +284,401 @@ var AsyncIteratorWrapperImpl = class _AsyncIteratorWrapperImpl {
|
|
|
526
284
|
}
|
|
527
285
|
}
|
|
528
286
|
};
|
|
287
|
+
function streamOf(source) {
|
|
288
|
+
return new AsyncIteratorWrapperImpl(source);
|
|
289
|
+
}
|
|
290
|
+
var stream;
|
|
291
|
+
((stream2) => {
|
|
292
|
+
function ofArrayPromise(p) {
|
|
293
|
+
async function* __ofArrayPromise(p2) {
|
|
294
|
+
const items = await p2;
|
|
295
|
+
for (const item of items) {
|
|
296
|
+
yield item;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return streamOf(__ofArrayPromise(p));
|
|
300
|
+
}
|
|
301
|
+
stream2.ofArrayPromise = ofArrayPromise;
|
|
302
|
+
})(stream || (stream = {}));
|
|
529
303
|
|
|
530
304
|
// src/util/file_tree_clustering.ts
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
305
|
+
var graph;
|
|
306
|
+
((graph2) => {
|
|
307
|
+
function buildGraph(files) {
|
|
308
|
+
const result = {
|
|
309
|
+
path: [],
|
|
310
|
+
children: {},
|
|
311
|
+
value: [],
|
|
312
|
+
size: 0
|
|
313
|
+
};
|
|
314
|
+
files.forEach((file) => {
|
|
315
|
+
let targetDir = result;
|
|
316
|
+
file.arr.slice(0, -1).forEach((pathSegment) => {
|
|
317
|
+
targetDir.size++;
|
|
318
|
+
if (!Object.prototype.hasOwnProperty.call(targetDir.children, pathSegment)) {
|
|
319
|
+
targetDir.children[pathSegment] = {
|
|
320
|
+
path: targetDir.path.concat([pathSegment]),
|
|
321
|
+
children: {},
|
|
322
|
+
value: [],
|
|
323
|
+
size: 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
targetDir = targetDir.children[pathSegment];
|
|
327
|
+
});
|
|
328
|
+
targetDir.value.push(file);
|
|
329
|
+
});
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
graph2.buildGraph = buildGraph;
|
|
333
|
+
function flatten(graphNode) {
|
|
334
|
+
if (Object.keys(graphNode.children).length === 0) {
|
|
335
|
+
return graphNode.value;
|
|
543
336
|
}
|
|
544
|
-
let
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
337
|
+
let fileInfos = Object.values(graphNode.children).flatMap((child) => flatten(child));
|
|
338
|
+
return fileInfos.concat(graphNode.value);
|
|
339
|
+
}
|
|
340
|
+
function bubbleMicroLeftovers(graphNode, clusterMaxSize, clusterMinSize) {
|
|
341
|
+
if (Object.keys(graphNode.children).length === 0) {
|
|
342
|
+
return false;
|
|
551
343
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
});
|
|
570
|
-
changes = true;
|
|
571
|
-
if (remainingFiles.length === 0) {
|
|
572
|
-
return [
|
|
573
|
-
{
|
|
574
|
-
path: originalCluster.path.concat([mf]),
|
|
575
|
-
files: newClusterFiles,
|
|
576
|
-
isLeftovers: false
|
|
577
|
-
}
|
|
578
|
-
];
|
|
579
|
-
}
|
|
580
|
-
if (newClusterFiles.length < clusterMinSize || remainingFiles.length < clusterMinSize) {
|
|
581
|
-
return [{ ...originalCluster, isUnclusterable: true }];
|
|
344
|
+
if (graphNode.size < clusterMaxSize) {
|
|
345
|
+
let allFiles = flatten(graphNode);
|
|
346
|
+
graphNode.children = {};
|
|
347
|
+
graphNode.value = allFiles;
|
|
348
|
+
graphNode.size = allFiles.length;
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
let result = false;
|
|
352
|
+
Object.entries(graphNode.children).forEach(([k, child]) => {
|
|
353
|
+
if (child.value.length < clusterMinSize) {
|
|
354
|
+
graphNode.value.push(...child.value);
|
|
355
|
+
graphNode.size += child.value.length;
|
|
356
|
+
child.size -= child.value.length;
|
|
357
|
+
child.value = [];
|
|
358
|
+
if (child.size === 0) {
|
|
359
|
+
delete graphNode.children[k];
|
|
360
|
+
result = true;
|
|
582
361
|
}
|
|
583
|
-
return [
|
|
584
|
-
{
|
|
585
|
-
path: originalCluster.path.concat([mf]),
|
|
586
|
-
files: newClusterFiles,
|
|
587
|
-
isLeftovers: false
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
path: originalCluster.path,
|
|
591
|
-
files: remainingFiles,
|
|
592
|
-
isLeftovers: true
|
|
593
|
-
}
|
|
594
|
-
];
|
|
595
|
-
} else {
|
|
596
|
-
return [originalCluster];
|
|
597
362
|
}
|
|
598
363
|
});
|
|
364
|
+
return result;
|
|
599
365
|
}
|
|
600
|
-
|
|
366
|
+
graph2.bubbleMicroLeftovers = bubbleMicroLeftovers;
|
|
367
|
+
function unpackSmallest(graphNode, clusterMaxSize, clusterMinSize) {
|
|
368
|
+
let childrenSortedAsc = Object.entries(graphNode.children).sort((a, b) => a[1].size - b[1].size);
|
|
369
|
+
if (childrenSortedAsc.length === 0) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
let [k, child] = childrenSortedAsc[0];
|
|
373
|
+
let canFit = child.size + graphNode.value.length <= clusterMaxSize;
|
|
374
|
+
let cannotIsolate = graphNode.value.length < clusterMinSize;
|
|
375
|
+
if (canFit && cannotIsolate) {
|
|
376
|
+
graphNode.value.push(...flatten(child));
|
|
377
|
+
graphNode.size++;
|
|
378
|
+
delete graphNode.children[k];
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
function bubbleMicroLeftoversRecursive(graphNode, clusterMaxSize, clusterMinSize) {
|
|
384
|
+
let changed = Object.values(graphNode.children).map((child) => {
|
|
385
|
+
bubbleMicroLeftoversRecursive(child, clusterMaxSize, clusterMinSize);
|
|
386
|
+
}).find(Boolean) || false;
|
|
387
|
+
changed = unpackSmallest(graphNode, clusterMaxSize, clusterMinSize) || changed;
|
|
388
|
+
changed = bubbleMicroLeftovers(graphNode, clusterMaxSize, clusterMinSize) || changed;
|
|
389
|
+
return changed;
|
|
390
|
+
}
|
|
391
|
+
graph2.bubbleMicroLeftoversRecursive = bubbleMicroLeftoversRecursive;
|
|
392
|
+
function collect(graphNode) {
|
|
393
|
+
let result = [];
|
|
394
|
+
function collectRecursive(node) {
|
|
395
|
+
result.push(node);
|
|
396
|
+
Object.values(node.children).forEach((child) => {
|
|
397
|
+
collectRecursive(child);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
collectRecursive(graphNode);
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
graph2.collect = collect;
|
|
404
|
+
})(graph || (graph = {}));
|
|
405
|
+
function clusterFiles(files, clusterMaxSize, clusterMinSize) {
|
|
406
|
+
let fileInfos = files.sort().map((it) => {
|
|
407
|
+
let arr = it.split("/");
|
|
408
|
+
return { arr, str: it };
|
|
409
|
+
});
|
|
410
|
+
let graphNode = graph.buildGraph(fileInfos);
|
|
411
|
+
while (graph.bubbleMicroLeftoversRecursive(graphNode, clusterMaxSize, clusterMinSize)) {
|
|
412
|
+
}
|
|
413
|
+
let subclusters = graph.collect(graphNode).filter((it) => it.value.length > 0).sort((a, b) => b.path.join("/").localeCompare(a.path.join("/"))).map((it) => ({
|
|
414
|
+
path: it.path,
|
|
415
|
+
files: it.value,
|
|
416
|
+
isLeftovers: it.size > clusterMinSize,
|
|
417
|
+
isUnclusterable: it.size <= clusterMinSize
|
|
418
|
+
}));
|
|
419
|
+
return subclusters.map((cluster) => {
|
|
601
420
|
let files2 = cluster.files;
|
|
602
|
-
let
|
|
421
|
+
let path6 = cluster.path;
|
|
603
422
|
let files1 = files2.map((it) => it.str);
|
|
604
423
|
return {
|
|
605
|
-
path:
|
|
606
|
-
files: files1,
|
|
424
|
+
path: path6.join("/"),
|
|
425
|
+
files: files1.sort(),
|
|
607
426
|
weight: files1.length,
|
|
608
427
|
isLeftovers: cluster.isLeftovers
|
|
609
428
|
};
|
|
610
429
|
});
|
|
611
430
|
}
|
|
612
431
|
|
|
613
|
-
// src/
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
432
|
+
// src/util/dataset.ts
|
|
433
|
+
async function* distinctCount(source) {
|
|
434
|
+
const map = /* @__PURE__ */ new Map();
|
|
435
|
+
for await (const row of source) {
|
|
436
|
+
const key = JSON.stringify(row);
|
|
437
|
+
let count = row?.count ?? 1;
|
|
438
|
+
if (map.has(key)) {
|
|
439
|
+
map.get(key).count += count;
|
|
440
|
+
} else {
|
|
441
|
+
map.set(key, { row, count });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
for (const { row, count } of map.values()) {
|
|
445
|
+
yield [row, count];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/discovery.ts
|
|
450
|
+
var import_fs2 = __toESM(require("fs"));
|
|
451
|
+
var import_path2 = __toESM(require("path"));
|
|
452
|
+
|
|
453
|
+
// src/util/util.ts
|
|
454
|
+
var util;
|
|
455
|
+
((util2) => {
|
|
456
|
+
function distinct(arr) {
|
|
457
|
+
return [...new Set(arr)];
|
|
458
|
+
}
|
|
459
|
+
util2.distinct = distinct;
|
|
460
|
+
function daysAgo(epoch) {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
const diff = now - epoch * 1e3;
|
|
463
|
+
return Math.floor(diff / (1e3 * 60 * 60 * 24));
|
|
464
|
+
}
|
|
465
|
+
util2.daysAgo = daysAgo;
|
|
466
|
+
function bucket(n, buckets) {
|
|
467
|
+
for (let i = 1; i < buckets.length; i++) {
|
|
468
|
+
if (n > buckets[i - 1] && n < buckets[i]) return buckets[i - 1];
|
|
469
|
+
}
|
|
470
|
+
return -1;
|
|
471
|
+
}
|
|
472
|
+
util2.bucket = bucket;
|
|
473
|
+
function yyyyMM(epoch) {
|
|
474
|
+
const date = new Date(epoch * 1e3);
|
|
475
|
+
let yyyyStr = date.getFullYear().toString();
|
|
476
|
+
let MMStr = (date.getMonth() + 1 / 4).toString().padStart(1, "0");
|
|
477
|
+
return parseInt(yyyyStr) * 10 + parseInt(MMStr);
|
|
478
|
+
}
|
|
479
|
+
util2.yyyyMM = yyyyMM;
|
|
480
|
+
})(util || (util = {}));
|
|
481
|
+
|
|
482
|
+
// src/discovery.ts
|
|
483
|
+
function getDirectories(absoluteDirPath) {
|
|
484
|
+
if (!import_fs2.default.existsSync(absoluteDirPath) || !import_fs2.default.statSync(absoluteDirPath).isDirectory()) return [];
|
|
617
485
|
const ignoredDirs = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
618
486
|
try {
|
|
619
|
-
return
|
|
487
|
+
return import_fs2.default.readdirSync(absoluteDirPath, { withFileTypes: true }).filter((dirent) => dirent.isDirectory() && !ignoredDirs.has(dirent.name)).map((dirent) => import_path2.default.join(absoluteDirPath, dirent.name));
|
|
620
488
|
} catch (error) {
|
|
621
|
-
console.error(`Could not read directory: ${
|
|
489
|
+
console.error(`Could not read directory: ${absoluteDirPath}`);
|
|
622
490
|
return [];
|
|
623
491
|
}
|
|
624
492
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
493
|
+
function findRepositories(absolutePath, depth) {
|
|
494
|
+
if (depth <= 0) return [];
|
|
495
|
+
if (!import_path2.default.isAbsolute(absolutePath)) throw new Error(`Path must be absolute: ${absolutePath}`);
|
|
496
|
+
if (!import_fs2.default.existsSync(absolutePath)) throw new Error(`Path does not exist: ${absolutePath}`);
|
|
497
|
+
if (!import_fs2.default.statSync(absolutePath).isDirectory()) throw new Error(`Path is not a directory: ${absolutePath}`);
|
|
498
|
+
if (isGitRepo(absolutePath)) return [absolutePath];
|
|
499
|
+
let result = getDirectories(absolutePath).flatMap((dir) => findRepositories(dir, depth - 1));
|
|
500
|
+
return util.distinct(result).sort();
|
|
501
|
+
}
|
|
502
|
+
function getRepoPathsToProcess(inputPaths) {
|
|
503
|
+
let repos = inputPaths.map((it) => import_path2.default.resolve(it)).flatMap((it) => findRepositories(it, 3));
|
|
504
|
+
return util.distinct(repos).sort();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/progress.ts
|
|
508
|
+
var process2 = __toESM(require("node:process"));
|
|
509
|
+
var Progress = class {
|
|
510
|
+
constructor() {
|
|
511
|
+
this.progress = {};
|
|
512
|
+
this.messages = {};
|
|
513
|
+
this.startTime = {};
|
|
514
|
+
this.currentInterval = void 0;
|
|
515
|
+
}
|
|
516
|
+
setProgress(name, current, max = void 0) {
|
|
517
|
+
this.progress[name] = [current, max ?? this.progress[name]?.[1] ?? void 0];
|
|
518
|
+
if (!this.startTime[name]) this.startTime[name] = Date.now();
|
|
519
|
+
}
|
|
520
|
+
setMessage(name, message) {
|
|
521
|
+
this.messages[name] = message;
|
|
522
|
+
}
|
|
523
|
+
stop(name) {
|
|
524
|
+
delete this.progress[name];
|
|
525
|
+
delete this.messages[name];
|
|
526
|
+
delete this.startTime[name];
|
|
527
|
+
}
|
|
528
|
+
destroy() {
|
|
529
|
+
Object.keys(this.progress).forEach((key) => this.stop(key));
|
|
530
|
+
clearInterval(this.currentInterval);
|
|
531
|
+
}
|
|
532
|
+
showProgress(period) {
|
|
533
|
+
this.currentInterval = setInterval(() => {
|
|
534
|
+
const now = Date.now();
|
|
535
|
+
const progress2 = Object.entries(this.progress).map(([key, [value, max]]) => {
|
|
536
|
+
let eta = "?";
|
|
537
|
+
const startTime = this.startTime[key];
|
|
538
|
+
if (startTime && max !== void 0) {
|
|
539
|
+
const elapsed = (now - startTime) / 1e3;
|
|
540
|
+
const rate = value / elapsed;
|
|
541
|
+
const remaining = max - value;
|
|
542
|
+
const etaSeconds = Math.round(remaining / rate);
|
|
543
|
+
const days = Math.floor(etaSeconds / 86400);
|
|
544
|
+
const hours = Math.floor(etaSeconds % 86400 / 3600);
|
|
545
|
+
const minutes = Math.floor(etaSeconds % 3600 / 60);
|
|
546
|
+
const seconds = etaSeconds % 60;
|
|
547
|
+
const parts = [];
|
|
548
|
+
if (days > 0) parts.push(`${days}d`);
|
|
549
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
550
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
551
|
+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
|
552
|
+
eta = ` ETA: ${parts.join("")}`;
|
|
553
|
+
}
|
|
554
|
+
return `${key}: [${value}/${max ?? "?"}]${eta} ${this.messages[key] ?? ""}`;
|
|
555
|
+
}).join(", ");
|
|
556
|
+
if (progress2.length === 0) return;
|
|
557
|
+
process2.stderr.clearLine(0, () => {
|
|
558
|
+
process2.stderr.write("\r" + progress2);
|
|
559
|
+
});
|
|
560
|
+
}, period);
|
|
633
561
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/vfs.ts
|
|
565
|
+
var import_fs3 = __toESM(require("fs"));
|
|
566
|
+
var import_path3 = __toESM(require("path"));
|
|
567
|
+
var RealFileSystemImpl = class {
|
|
568
|
+
constructor(basePath) {
|
|
569
|
+
this.basePath = import_path3.default.normalize(basePath);
|
|
641
570
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
"
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
571
|
+
async read(filePath) {
|
|
572
|
+
return import_fs3.default.readFileSync(this.resolve(filePath), "utf8");
|
|
573
|
+
}
|
|
574
|
+
async write(filePath, content) {
|
|
575
|
+
let path1 = import_path3.default.resolve(this.resolve(filePath), "..");
|
|
576
|
+
import_fs3.default.mkdirSync(path1, { recursive: true });
|
|
577
|
+
import_fs3.default.writeFileSync(this.resolve(filePath), content);
|
|
578
|
+
}
|
|
579
|
+
async append(filePath, content) {
|
|
580
|
+
return new Promise((resolve2, reject) => {
|
|
581
|
+
import_fs3.default.appendFile(this.resolve(filePath), content, (err) => {
|
|
582
|
+
if (!err) {
|
|
583
|
+
resolve2(void 0);
|
|
584
|
+
} else {
|
|
585
|
+
reject(err);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
resolve(filePath) {
|
|
591
|
+
return import_path3.default.normalize(`${this.basePath}/${filePath}`);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/index.ts
|
|
596
|
+
var sigintCaught = false;
|
|
597
|
+
var progress = null;
|
|
598
|
+
var dataDir = new RealFileSystemImpl("./.git-stats");
|
|
599
|
+
async function* getRepositoryFiles(repoRelativePath) {
|
|
600
|
+
console.error(`
|
|
601
|
+
Processing repository: ${repoRelativePath || "."}`);
|
|
602
|
+
const absoluteRepoPath = path5.resolve(process.cwd(), repoRelativePath);
|
|
603
|
+
const repoName = path5.basename(absoluteRepoPath);
|
|
604
|
+
let revisionBoundary = await findRevision(absoluteRepoPath, 1e3);
|
|
605
|
+
const files = await git_ls_files(absoluteRepoPath, ".");
|
|
606
|
+
let minClusterSize = Math.floor(Math.max(2, files.length / 100));
|
|
607
|
+
let maxClusterSize = Math.round(Math.max(20, files.length / 30));
|
|
608
|
+
console.error(`Found ${files.length} files to analyze in '${repoName}'...`);
|
|
653
609
|
console.error(`Clustering ${files.length} into ${minClusterSize}..${maxClusterSize}+ sized chunks`);
|
|
654
610
|
const filesClustered = clusterFiles(
|
|
655
611
|
files,
|
|
656
612
|
maxClusterSize,
|
|
657
613
|
minClusterSize
|
|
658
614
|
);
|
|
659
|
-
console.error(filesClustered.map((it) => `${it.path}
|
|
660
|
-
let
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
615
|
+
console.error(filesClustered.map((it) => `${it.path} (${it.weight})`));
|
|
616
|
+
let filesShuffled = filesClustered.flatMap(
|
|
617
|
+
(cluster) => cluster.files.flatMap((file) => ({
|
|
618
|
+
file,
|
|
619
|
+
cluster: cluster.path
|
|
620
|
+
}))
|
|
621
|
+
);
|
|
622
|
+
for (let i = filesShuffled.length - 1; i > 0; i--) {
|
|
623
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
624
|
+
[filesShuffled[i], filesShuffled[j]] = [filesShuffled[j], filesShuffled[i]];
|
|
625
|
+
}
|
|
626
|
+
for (let i = 0; i < filesShuffled.length; i++) {
|
|
664
627
|
if (sigintCaught) break;
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
628
|
+
const currentFile = filesShuffled[i];
|
|
629
|
+
progress?.setProgress("File", i + 1, files.length);
|
|
630
|
+
progress?.setMessage("File", currentFile.file);
|
|
631
|
+
yield {
|
|
632
|
+
repo: path5.basename(absoluteRepoPath),
|
|
633
|
+
file: currentFile.file,
|
|
634
|
+
rev: revisionBoundary,
|
|
635
|
+
cluster: currentFile.cluster
|
|
636
|
+
};
|
|
674
637
|
}
|
|
675
638
|
process.stderr.write(" ".repeat(process.stderr.columns || 80) + "\r");
|
|
676
639
|
console.error(`Analysis complete for '${repoName}'.`);
|
|
677
640
|
}
|
|
678
|
-
function
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
return Math.floor(diff / (1e3 * 60 * 60 * 24));
|
|
682
|
-
}
|
|
683
|
-
function bucket(n, buckets) {
|
|
684
|
-
for (let i = 1; i < buckets.length; i++) {
|
|
685
|
-
if (n > buckets[i - 1] && n < buckets[i]) return buckets[i - 1];
|
|
686
|
-
}
|
|
687
|
-
return -1;
|
|
688
|
-
}
|
|
689
|
-
async function doProcessFile1(repoRoot, filePath, revisionBoundary) {
|
|
690
|
-
if (!filePath) return [];
|
|
691
|
-
const absPath = path4.join(repoRoot, filePath);
|
|
641
|
+
async function doProcessFile(absoluteRepoRoot, repoRelativeFilePath, revisionBoundary) {
|
|
642
|
+
if (!repoRelativeFilePath) return [];
|
|
643
|
+
const absoluteFilePath = path5.join(absoluteRepoRoot, repoRelativeFilePath);
|
|
692
644
|
let stat = null;
|
|
693
645
|
try {
|
|
694
|
-
stat =
|
|
646
|
+
stat = fs5.statSync(absoluteFilePath);
|
|
695
647
|
} catch (e) {
|
|
696
|
-
console.error(`Fail get stats for file ${
|
|
648
|
+
console.error(`Fail get stats for file ${absoluteFilePath}`, e.stack || e.message || e);
|
|
697
649
|
}
|
|
698
650
|
if (!stat || !stat.isFile() || stat.size === 0) return [];
|
|
699
651
|
const result = [];
|
|
700
|
-
for (const item of await git_blame_porcelain(
|
|
701
|
-
if (revisionBoundary === item
|
|
702
|
-
item
|
|
703
|
-
item
|
|
704
|
-
item
|
|
705
|
-
}
|
|
706
|
-
const lang = path4.extname(filePath) || "Other";
|
|
707
|
-
let days_bucket = bucket(daysAgo(item[1]), [0, 30, 300, 1e3, 1e6]);
|
|
708
|
-
if (days_bucket != -1) {
|
|
709
|
-
result.push([item[0], days_bucket, lang, filePath, repoRoot]);
|
|
652
|
+
for (const item of await git_blame_porcelain(repoRelativeFilePath, absoluteRepoRoot, ["author", "committer-time", "commit"], !!revisionBoundary ? revisionBoundary + "..HEAD" : void 0)) {
|
|
653
|
+
if (revisionBoundary === item.commit) {
|
|
654
|
+
item.author = "Legacy";
|
|
655
|
+
item.time = 0;
|
|
656
|
+
item.commit = "0".repeat(40);
|
|
710
657
|
}
|
|
658
|
+
result.push({
|
|
659
|
+
repo: absoluteRepoRoot,
|
|
660
|
+
file: path5.basename(repoRelativeFilePath),
|
|
661
|
+
author: item.author,
|
|
662
|
+
commit: item.commit,
|
|
663
|
+
time: item.time,
|
|
664
|
+
year: new Date(item.time * 1e3).getFullYear(),
|
|
665
|
+
month: new Date(item.time * 1e3).getMonth() + 1,
|
|
666
|
+
lang: path5.extname(repoRelativeFilePath) || "Other"
|
|
667
|
+
});
|
|
711
668
|
}
|
|
712
669
|
return result;
|
|
713
670
|
}
|
|
714
|
-
async function
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
} else {
|
|
721
|
-
map.set(key, { row, count: 1 });
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
for (const { row, count } of map.values()) {
|
|
725
|
-
yield [...row, count];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
function getRepoPathsToProcess(inputPaths) {
|
|
729
|
-
let repoPathsToProcess = inputPaths.flatMap((it) => findRepositories(it, 3));
|
|
730
|
-
repoPathsToProcess = [...new Set(repoPathsToProcess)].sort();
|
|
731
|
-
if (repoPathsToProcess.length === 0) {
|
|
732
|
-
throw new Error("No git repositories found to analyze.");
|
|
733
|
-
}
|
|
734
|
-
console.error(`Found ${repoPathsToProcess.length} repositories to analyze:`);
|
|
735
|
-
repoPathsToProcess.forEach((p) => console.error(`- ${p || "."}`));
|
|
736
|
-
return repoPathsToProcess;
|
|
671
|
+
async function runScan1(args) {
|
|
672
|
+
const inputPaths = args && args.length > 0 ? args : ["."];
|
|
673
|
+
let repoPathsToProcess = getRepoPathsToProcess(inputPaths);
|
|
674
|
+
await dataDir.write("known_repositories.txt", repoPathsToProcess.join("\n") + "\n");
|
|
675
|
+
let dataSet = streamOf(AsyncGeneratorUtil.of(repoPathsToProcess)).flatMap((repoRelativePath) => getRepositoryFiles(repoRelativePath)).flatMap((fileInfo) => stream.ofArrayPromise(doProcessFile(fileInfo.repo, fileInfo.file, fileInfo.rev)).get()).map((it) => [it.author, it.time, it.lang, it.cluster, it.repo]).get();
|
|
676
|
+
return distinctCount(dataSet);
|
|
737
677
|
}
|
|
738
678
|
async function runScan(args) {
|
|
739
|
-
|
|
679
|
+
let [keys, paths] = extractArgKeys(args);
|
|
680
|
+
progress = new Progress();
|
|
681
|
+
progress.showProgress(300);
|
|
740
682
|
process.on("SIGINT", () => {
|
|
741
683
|
if (sigintCaught) {
|
|
742
684
|
console.error("\nForcing exit.");
|
|
@@ -745,23 +687,24 @@ async function runScan(args) {
|
|
|
745
687
|
sigintCaught = true;
|
|
746
688
|
console.error("\nSignal received. Finishing current file then stopping. Press Ctrl+C again to exit immediately.");
|
|
747
689
|
});
|
|
748
|
-
|
|
749
|
-
let repoPathsToProcess = getRepoPathsToProcess(inputPaths);
|
|
750
|
-
await tmpVfs.write("data.jsonl", "");
|
|
751
|
-
let dataSet = new AsyncIteratorWrapperImpl(AsyncGeneratorUtil.of(repoPathsToProcess)).flatMap((repoPath) => forEachRepoFile(repoPath, doProcessFile1)).map((it) => [it[0], it[1], it[2], it[5], path4.basename(it[4])]).get();
|
|
752
|
-
let aggregatedData1 = distinctCount(dataSet);
|
|
690
|
+
let aggregatedData1 = await runScan1(paths);
|
|
753
691
|
let aggregatedData = await AsyncGeneratorUtil.collect(aggregatedData1);
|
|
754
|
-
|
|
692
|
+
progress.destroy();
|
|
693
|
+
if (keys.includes("stdout")) {
|
|
694
|
+
aggregatedData.forEach((it) => console.log(JSON.stringify(it)));
|
|
695
|
+
} else {
|
|
696
|
+
aggregatedData.forEach((it) => dataDir.append("data.jsonl", JSON.stringify(it) + "\n"));
|
|
697
|
+
}
|
|
755
698
|
}
|
|
756
699
|
async function runHtml(args) {
|
|
757
|
-
const
|
|
758
|
-
const
|
|
759
|
-
if (!
|
|
760
|
-
console.error(`Input data file not found: ${
|
|
700
|
+
const absoluteInputPath = args[0] || path5.resolve("./.git-stats/data.jsonl");
|
|
701
|
+
const absoluteOutHtml = path5.resolve("./.git-stats/report.html");
|
|
702
|
+
if (!fs5.existsSync(absoluteInputPath)) {
|
|
703
|
+
console.error(`Input data file not found: ${absoluteInputPath}`);
|
|
761
704
|
process.exitCode = 1;
|
|
762
705
|
return;
|
|
763
706
|
}
|
|
764
|
-
const lines =
|
|
707
|
+
const lines = fs5.readFileSync(absoluteInputPath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
765
708
|
const aggregatedData = lines.map((line) => {
|
|
766
709
|
try {
|
|
767
710
|
return JSON.parse(line);
|
|
@@ -769,29 +712,92 @@ async function runHtml(args) {
|
|
|
769
712
|
return null;
|
|
770
713
|
}
|
|
771
714
|
}).filter(Boolean);
|
|
772
|
-
generateHtmlReport(aggregatedData,
|
|
773
|
-
console.error(`HTML report generated: ${
|
|
715
|
+
generateHtmlReport(aggregatedData, absoluteOutHtml);
|
|
716
|
+
console.error(`HTML report generated: ${absoluteOutHtml}`);
|
|
774
717
|
}
|
|
775
|
-
function
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
718
|
+
async function* forEachStdinLine(consumer) {
|
|
719
|
+
const rl = readline.createInterface({
|
|
720
|
+
input: process.stdin,
|
|
721
|
+
output: process.stdout,
|
|
722
|
+
terminal: false
|
|
723
|
+
});
|
|
724
|
+
for await (const line of rl) {
|
|
725
|
+
if (!line.trim()) continue;
|
|
726
|
+
try {
|
|
727
|
+
consumer(line);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
console.error(`Error parsing line: ${line}`, error);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
yield null;
|
|
733
|
+
}
|
|
734
|
+
async function runSlice(args) {
|
|
735
|
+
let cols = args.map((it) => parseInt(it));
|
|
736
|
+
let result = {};
|
|
737
|
+
await forEachStdinLine((it) => {
|
|
738
|
+
const data = JSON.parse(it);
|
|
739
|
+
console.log(data);
|
|
740
|
+
let n = result;
|
|
741
|
+
for (let col of cols) {
|
|
742
|
+
n[data[col]] = n?.[data[col]] ?? {};
|
|
743
|
+
n = n[data[col]];
|
|
744
|
+
n.count = (n?.count ?? 0) + data[data.length - 1];
|
|
745
|
+
n.values = n?.values ?? {};
|
|
746
|
+
n = n.values;
|
|
747
|
+
}
|
|
748
|
+
}).next();
|
|
749
|
+
console.log(JSON.stringify(result, null, 2));
|
|
750
|
+
}
|
|
751
|
+
function extractArgKeys(args) {
|
|
752
|
+
const keys = [];
|
|
753
|
+
const values = [];
|
|
754
|
+
for (let i = 0; i < args.length; i++) {
|
|
755
|
+
const arg = args[i];
|
|
756
|
+
if (arg.startsWith("--")) {
|
|
757
|
+
keys.push(arg.substring(2));
|
|
758
|
+
} else {
|
|
759
|
+
values.push(arg);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return [keys, values];
|
|
782
763
|
}
|
|
783
764
|
async function main() {
|
|
784
765
|
const argv = process.argv.slice(2);
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
766
|
+
let subcommand = argv[0];
|
|
767
|
+
let subcommandsMenu = {
|
|
768
|
+
"html": {
|
|
769
|
+
description: "Generates an HTML report from the aggregated data.",
|
|
770
|
+
usage: "git-stats html [input-data-file]"
|
|
771
|
+
},
|
|
772
|
+
"scan": {
|
|
773
|
+
description: "Scans a directory tree for Git repositories and generates aggregated data.",
|
|
774
|
+
usage: "git-stats scan [input-dir] > {output-file}.jsonl"
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
if (subcommand === "scan") {
|
|
778
|
+
await runScan(argv.slice(1));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (subcommand === "html") {
|
|
782
|
+
await runHtml(argv.slice(1));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (subcommand === "slice") {
|
|
786
|
+
await runSlice(argv.slice(1));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
console.error(`Usage: git-stats <subcommand> [args]
|
|
790
|
+
|
|
791
|
+
Available subcommands:`);
|
|
792
|
+
for (const [name, { description, usage }] of Object.entries(subcommandsMenu)) {
|
|
793
|
+
console.error(`- ${name}: ${description}
|
|
794
|
+
Usage: ${usage}`);
|
|
791
795
|
}
|
|
792
796
|
}
|
|
793
797
|
main().catch(console.error);
|
|
794
798
|
// Annotate the CommonJS export names for ESM import in node:
|
|
795
799
|
0 && (module.exports = {
|
|
796
|
-
|
|
800
|
+
dataDir,
|
|
801
|
+
progress,
|
|
802
|
+
runScan1
|
|
797
803
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vladimirshefer/git-stats",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A CLI to generate git blame stats for a repository",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^20.0.0",
|
|
24
|
+
"@vladimirshefer/git-stats--html-ui": "*",
|
|
24
25
|
"vitest": "^1.6.0",
|
|
25
|
-
"esbuild": "0.25.
|
|
26
|
+
"esbuild": "0.25.2",
|
|
26
27
|
"typescript": "^5.0.0"
|
|
27
28
|
}
|
|
28
29
|
}
|