Source: git.js

import * as TYPES from '#types/types.js' // eslint-disable-line
import gitUrlParse from 'git-url-parse'
import ora from 'ora'
import { $ } from 'execa'

/**
 * @ignore
 * @typedef {TYPES.GIT_DATA} GIT_DATA {@link GIT_DATA}
 * @typedef {TYPES.DATA} DATA {@link DATA}
 * @typedef {TYPES.COMMIT_MESSAGE} COMMIT_MESSAGE {@link COMMIT_MESSAGE}
 */

/**
 * #### Check is Git Repo
 * @async
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isRepo () {
  try {
    const isRepo = await $`git rev-parse --is-inside-work-tree`
    if (isRepo.stdout === 'true') {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git root
 * @async
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isRoot () {
  try {
    const isGitdir = await $`git rev-parse --git-dir`
    if (/^\.(git)?$/.test(isGitdir.stdout.trim())) {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git commits
 * @async
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isCommits () {
  try {
    const isCommits = await $`git rev-parse --all`
    if (isCommits.stdout.trim() !== '') {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git tag in current commit
 * @async
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isTagInCurrentCommit () {
  try {
    const isTagInCurrentCommit = await $`git tag --contains`
    if (isTagInCurrentCommit.stdout.trim() !== '') {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git Remote
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isRemote () {
  try {
    const isRemote = await $`git remote`
    if (isRemote.stdout.trim() !== '') {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git branch detached
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isBranchDetached () {
  try {
    const statusData = await status()
    if (statusData && /\(no branch\)/.test(statusData)) {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git branch ahead
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isBranchAhead () {
  try {
    const statusData = await status()
    if (statusData && /ahead (\d+)/.exec(statusData) !== null) {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### Check is Git behind
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<boolean>}
 */
async function isBranchBehind () {
  try {
    const statusData = await status()
    if (statusData && /behind (\d+)/.exec(statusData) !== null) {
      return true
    } else {
      return false
    }
  } catch (error) {
    return false
  }
}

/**
 * #### save git hash
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<string>}
 */
async function hash () {
  try {
    const hash = await $`git rev-parse --short HEAD`
    if (hash.stdout.trim() !== '') {
      return hash.stdout.trim()
    } else {
      return ''
    }
  } catch (error) {
    return ''
  }
}

/**
 * #### save git branch
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<string>}
 */
async function branch () {
  try {
    const branch = await $`git rev-parse --abbrev-ref HEAD`
    if (branch.stdout.trim() !== '') {
      return branch.stdout.trim()
    } else {
      return ''
    }
  } catch (error) {
    return ''
  }
}

/**
 * #### save last git tag
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<string>}
 */
async function lastTag () {
  try {
    const lastTag = await $`git describe --abbrev=0 --tags`
    if (lastTag.stdout.trim() !== '') {
      return lastTag.stdout.trim()
    } else {
      return ''
    }
  } catch (error) {
    return ''
  }
}

/**
 * #### save git status
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<string>}
 */
async function status () {
  try {
    const status = await $`git status --porcelain -b -u --null`
    // trim and remove \x00
    // eslint-disable-next-line no-control-regex
    const statusData = status.stdout.trim().replace(/\x00/g, '')
    if (statusData !== '') {
      return statusData
    } else {
      return ''
    }
  } catch (error) {
    return ''
  }
}

/**
 * #### Save git branch upstream
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<string>}
 */
async function branchUpstream () {
  try {
    /** @type {string} */
    const branchName = await branch()
    const branchUpstream = await $`git rev-parse --abbrev-ref ${branchName}@{u}`
    if (branchUpstream.stdout) {
      return branchUpstream.stdout.trim()
    } else {
      return ''
    }
  } catch (error) {
    return ''
  }
}

/**
 * #### save staged files
 * @async
 * @memberof GIT
 * @returns {Promise<Array<string>>} staged files array or false
 */
async function stagedFiles () {
  try {
    const stagedFiles = await $`git diff --name-only --cached`
    if (stagedFiles.stdout.trim() !== '') {
      return stagedFiles.stdout.trim().split('\n')
    } else {
      return []
    }
  } catch (error) {
    return []
  }
}

/**
 * #### save changed files
 * @async
 * @memberof GIT
 * @returns {Promise<Array<string>>} staged files array or false
 */
async function changedFiles () {
  try {
    const stagedFiles = await $`git diff --name-only`
    if (stagedFiles.stdout.trim() !== '') {
      return stagedFiles.stdout.trim().split('\n')
    } else {
      return []
    }
  } catch (error) {
    return []
  }
}

/**
 * #### save git origin remote info
 * @async
 * @private
 * @memberof GIT
 * @returns {Promise<Object>}
 */
async function remote () {
  try {
    const remoteUrl = await $`git remote get-url origin`
    const parseremote = gitUrlParse(remoteUrl.stdout.trim())
    if (parseremote) {
      return parseremote
    } else {
      return {}
    }
  } catch (error) {
    return {}
  }
}

/**
 * #### Genereta git info
 * @async
 * @memberof GIT
 * @returns {Promise<GIT_DATA>}
 */
async function gitData () {
  /** @type {GIT_DATA} */
  const git = {
    isRepo: false,
    isRoot: false,
    isCommits: false,
    isTagInCurrentCommit: false,
    isRemote: false,
    isBranchDetached: false,
    isBranchAhead: false,
    isBranchBehind: false,
    hash: '',
    branch: '',
    lastTag: '',
    status: '',
    log: [],
    branchUpstream: '',
    stagedFiles: [],
    changedFiles: [],
    remote: {}
  }

  git.isRepo = await isRepo()
  if (git.isRepo) {
    git.isRoot = await isRoot()
  }
  if (git.isRepo && git.isRoot) {
    git.isCommits = await isCommits()

    if (git.isCommits && !git.isTagInCurrentCommit) {
      git.isRemote = await isRemote()
      git.hash = await hash()
      git.branch = await branch()
      git.lastTag = await lastTag()
      git.status = await status()
      git.branchUpstream = await branchUpstream()
      git.stagedFiles = await stagedFiles()
      git.changedFiles = await changedFiles()

      if (git.isRemote) {
        git.remote = await remote()
        git.isBranchDetached = await isBranchDetached()
        git.isBranchAhead = await isBranchAhead()
        git.isBranchBehind = await isBranchBehind()
      }
    }
  }
  return git
}

/**
 * #### git remote update
 * @async
 * @private
 * @memberof GIT
 * @returns undefined
 */
async function gitRemoteUpdate () {
  const spinner = ora('Sync remote repo with local').start()
  try {
    await $`git remote update`
    spinner.succeed()
  } catch (error) {
    spinner.fail()
    console.error(error)
  }
}

/**
 * #### update git remote data
 * @async
 * @memberof GIT
 * @param {DATA} data - data object
 * @returns {Promise<DATA>} local data
 */
async function gitRemoteDataUpdate (data) {
  if (data.git.isRemote) {
    await gitRemoteUpdate()
    data.git.isBranchDetached = await isBranchDetached()
    data.git.isBranchAhead = await isBranchAhead()
    data.git.isBranchBehind = await isBranchBehind()
  }
  return data
}

/**
 * #### git add files
 * @async
 * @memberof GIT
 * @param {Array<string>} files - files to git add
 * @returns undefined
 */
async function gitAdd (files) {
  try {
    if (files.length > 0) {
      for await (const file of files) {
        await $`git add ${file}`
      }
    }
  } catch (error) {
    console.error(error)
  }
}

/**
 * #### git commit release
 * @async
 * @memberof GIT
 * @param {string} msg - commit message
 * @returns undefined
 */
async function gitCommit (msg) {
  const spinner = ora('Commit release').start()
  try {
    await $`git commit -m ${msg}`
    spinner.succeed()
  } catch (error) {
    spinner.fail()
    console.error(error)
  }
}

/**
 * #### git push to remote
 * @async
 * @memberof GIT

 * @returns undefined
 */
async function gitPush () {
  const spinner = ora('Push to remote').start()
  try {
    await $`git push`
    spinner.succeed()
  } catch (error) {
    spinner.fail()
    console.error(error)
  }
}

/**
 * #### git tag
 * @async
 * @memberof GIT
 * @param {string} tag - tag
 * @returns undefined
 */
async function gitTag (tag) {
  try {
    await $`git tag ${tag}`
  } catch (error) {
    console.error(error)
  }
}

/**
 * #### git push tag to remote
 * @async
 * @memberof GIT
 * @returns undefined
 */
async function gitPushTag () {
  const spinner = ora('Push tag to remote').start()
  try {
    await $`git push --tags`
    spinner.succeed()
  } catch (error) {
    spinner.fail()
    console.error(error)
  }
}

/**
 * #### git log
 * parse git log and use it to generate changelog
 * @async
 * @private
 * @memberof GIT
 * @param {DATA} data - data object
 * @returns {Promise<string[]>} log array
 */
async function gitLog (data) {
  let lastTag = 'HEAD'
  if (data.git.lastTag !== '') {
    lastTag = `${data.git.lastTag}..HEAD`
  }
  /** @type {Array<string>} */
  let log = []
  try {
    const { stdout } = await $`git log ${lastTag} --format=%s#@sep@#%an#@sep@#%h#@sep@#%aI`
    if (stdout !== '') {
      log = stdout.split('\n')
    }
    return log
  } catch (error) {
    return log
  }
}

/**
 * #### parse git log and return each commit message as object with changelog properties
 * @private
 * @memberof GIT
 * @param {string} commit - commit message
 * @returns {COMMIT_MESSAGE} portal name|names
 */
function parseCommit (commit) {
  const commitArray = commit.split('#@sep@#')
  const prefix = ''
  const message = commitArray[0].trim()
  const name = commitArray[1].trim()
  const hash = commitArray[2].trim()
  const date = commitArray[3].trim()
  return { commit, prefix, message, hash, name, date }
}

/**
 * #### parse and save git log
 * @async
 * @memberof GIT
 * @param {DATA} data - data object
 * @returns undefined
 */
async function saveGitlog (data) {
  /** @type {Array<COMMIT_MESSAGE>} */
  const gitLogData = []
  const logs = await gitLog(data)
  if (logs.length > 0) {
    logs.forEach((logItem) => {
      gitLogData.push(parseCommit(logItem))
    })
  }
  data.git.log = gitLogData
}

export {
  isRepo,
  isRoot,
  isCommits,
  isTagInCurrentCommit,
  stagedFiles,
  changedFiles,
  gitData,
  gitRemoteDataUpdate,
  gitAdd,
  gitCommit,
  gitPush,
  gitTag,
  gitPushTag,
  saveGitlog
}

Table of contents