@softvisio/core
Version:
Softisio core
340 lines (265 loc) • 8.17 kB
JavaScript
import SemanticVersion from "#lib/semantic-version";
import GitFooters from "./footers.js";
import GitMessage from "./message.js";
// DOCS: https://www.conventionalcommits.org/
// DOCS: https://git-scm.com/docs/git-interpret-trailers
const COMMIT_SUBJECT_RE = /^(?<type>[\da-z-]+)(?:\((?<scope>[\da-z-]+)\))?(?<breaking>!)?: (?<subjectText>.+)/;
export default class GitCommit extends GitMessage {
#changeId;
#hash;
#abbrev;
#date;
#author;
#isHead;
#branch;
#branches;
#tags;
#parentHashes;
#type;
#scope;
#isBreakingChange;
#subjectText;
#bodyText;
#footers = new GitFooters();
#revertHash;
#links;
#fixes;
#commits = new Map();
#authors = new Set();
#releaseVersion;
constructor ( { message, hash, abbrev, date, author, isHead, branch, branches, tags, parentHashes } ) {
super();
this.#hash = hash || null;
this.#abbrev = abbrev || null;
this.#date = date
? new Date( date )
: null;
if ( author ) {
this.#author = author;
this.#authors.add( author );
}
this.#commits.set( abbrev, this );
this.#isHead = isHead;
this.#branch = branch;
// branches
if ( branches instanceof Set ) {
branches = [ ...branches ];
}
else if ( !Array.isArray( branches ) ) {
branches = [ branches ];
}
this.#branches = new Set( branches
.map( item => item?.trim() )
.filter( item => item )
.sort() );
// tags
if ( tags instanceof Set ) {
tags = [ ...tags ];
}
else if ( !Array.isArray( tags ) ) {
tags = [ tags ];
}
this.#tags = new Set( tags
.map( item => item?.trim() )
.filter( item => item )
.sort() );
// parentHashes
if ( parentHashes instanceof Set ) {
parentHashes = [ ...parentHashes ];
}
else if ( !Array.isArray( parentHashes ) ) {
parentHashes = [ parentHashes ];
}
this.#parentHashes = new Set( parentHashes
.map( item => item?.trim() )
.filter( item => item )
.sort() );
let subject, body;
// replace tabs, trim line trailing spaces
message = message
.replaceAll( "\t", " ".repeat( 4 ) )
.replaceAll( / +$/gm, "" )
.replaceAll( /\n{3,}/g, "\n\n" )
.trim();
// parse message
const idx = message.indexOf( "\n" );
if ( idx === -1 ) {
subject = message;
body = "";
}
else {
subject = message.slice( 0, idx );
body = message.slice( idx + 1 ).trim();
}
// parse subject
const match = subject.match( COMMIT_SUBJECT_RE );
this.#type = match?.groups?.type ?? "";
this.#scope = match?.groups?.scope ?? "";
this.#isBreakingChange = match
? !!match.groups.breaking
: false;
this.#subjectText = match
? match.groups.subjectText.trim()
: subject.trim();
// parse body
if ( body ) {
// find first footer
const match = body.match( /(?:^|\n{2,})(?:breaking[ -]change|[\da-z-]+) *: */i );
// no footers found
if ( !match ) {
this.#bodyText = body;
}
// footers found
else {
this.#bodyText = body.slice( 0, match.index );
const footers = body.slice( match.index ).split( /^(breaking[ -]change|[\da-z-]+) *: */im );
for ( let n = 1; n < footers.length; n += 2 ) {
// breaking change footer
if ( /^breaking[ -]change$/i.test( footers[ n ] ) ) {
this.#isBreakingChange = true;
const text = footers[ n + 1 ].trim();
if ( text ) {
if ( this.#bodyText ) {
this.#bodyText += "\n\n" + text;
}
else {
this.#bodyText = text;
}
}
}
// other footer
else {
this.#footers.add( footers[ n ], footers[ n + 1 ] );
}
}
}
}
else {
this.#bodyText = "";
}
}
// static
static new ( commit ) {
if ( commit instanceof this ) return commit;
return new this( commit );
}
static get compare () {
return ( a, b ) => this.new( a ).compare( b );
}
// properties
get changeId () {
if ( this.#changeId == null ) {
this.#changeId = this.#scope + "/" + this.#subjectText;
}
return this.#changeId;
}
get hash () {
return this.#hash;
}
get abbrev () {
return this.#abbrev || this.#hash;
}
get date () {
return this.#date;
}
get author () {
return this.#author;
}
get isHead () {
return this.#isHead;
}
get isDetachedHead () {
return this.isHead && !this.branch;
}
get isBranchHead () {
return this.isHead && this.branch;
}
get branch () {
return this.#branch;
}
get branches () {
return this.#branches;
}
get tags () {
return this.#tags;
}
get isBreakingChange () {
return this.#isBreakingChange;
}
get type () {
return this.#type;
}
get scope () {
return this.#scope;
}
get isMerge () {
return this.#parentHashes.size > 1;
}
get subjectText () {
return this.#subjectText;
}
get bodyText () {
return this.#bodyText;
}
get footers () {
return this.#footers;
}
get revertHash () {
if ( this.#revertHash === undefined ) {
this.#revertHash = null;
if ( this.isRevert ) {
const match = this.body.match( /This reverts commit ([\da-f]+)/ );
if ( match ) {
this.#revertHash = match[ 1 ];
}
}
}
return this.#revertHash;
}
get fixes () {
if ( this.#fixes === undefined ) {
const links = [];
for ( const match of this.message.matchAll( /(?:^|\W)(?:close[ds]?|fix(?:es|ed)?|resolve(?:s|ds)?) *:? +((?:[\w.-]+\/[\w.-]+)?#\d+)(?:\W|$)/gim ) ) {
links.push( match[ 1 ] );
}
this.#fixes = new Set( links.sort( this.constructor.compareLinks ) );
}
return this.#fixes;
}
get links () {
if ( this.#links === undefined ) {
const links = [];
for ( const match of this.message.matchAll( /(?:^|\W)((?:[\w.-]+\/[\w.-]+)?#\d+)(?:\W|$)/gm ) ) {
links.push( match[ 1 ] );
}
this.#links = new Set( links.sort( this.constructor.compareLinks ) );
}
return this.#links;
}
get authors () {
return this.#authors;
}
get commits () {
return this.#commits;
}
get isRelease () {
return Boolean( this.releaseVersion );
}
get releaseVersion () {
if ( this.#releaseVersion === undefined ) {
this.#releaseVersion = null;
for ( const tag of this.tags ) {
if ( SemanticVersion.isValid( tag ) ) {
this.#releaseVersion = SemanticVersion.new( tag );
break;
}
}
}
return this.#releaseVersion;
}
// public
compare ( commit ) {
commit = this.constructor.new( commit );
return super.compare( commit ) || this.hash?.localeCompare( commit.hash ) || 0;
}
}