PSGitUtils.psm1

[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

$new = [char]::ConvertFromUtf32('0x1F195');
$bug = [char]::ConvertFromUtf32('0x1F41B');
$boom = [char]::ConvertFromUtf32('0x1F4A5');
$lipstick = [char]::ConvertFromUtf32('0x1F484');
$speech_balloon = [char]::ConvertFromUtf32('0x1F4AC');
$art = [char]::ConvertFromUtf32('0x1F3A8');
$white_check_mark = [char]::ConvertFromUtf32('0x2705');
$wrench = [char]::ConvertFromUtf32('0x1F527');
$arrow_up = [char]::ConvertFromUtf32('0x2B06');
$arrow_down = [char]::ConvertFromUtf32('0x2B07');
$heavy_plus_sign = [char]::ConvertFromUtf32('0x2795');
$heavy_minus_sign = [char]::ConvertFromUtf32('0x2796');
$see_no_evil = [char]::ConvertFromUtf32('0x1F648');
$pencil2 = [char]::ConvertFromUtf32('0x0270f');
$memo = [char]::ConvertFromUtf32('0x1f4dd');
$bulb = [char]::ConvertFromUtf32('0x1f4a1');
$zap = [char]::ConvertFromUtf32('0x26A1');
$fire = [char]::ConvertFromUtf32('0x1F525');
$recycle = [char]::ConvertFromUtf32('0x267B');
$truck = [char]::ConvertFromUtf32('0x1F69A');
$tada = [char]::ConvertFromUtf32('0x1F389');
$hammer = [char]::ConvertFromUtf32('0x1f528');
$construction = [char]::ConvertFromUtf32('0x1f6a7');
$children_crossing = [char]::ConvertFromUtf32('0x1f6b8'); #🚸
$construction_worker = [char]::ConvertFromUtf32('0x1f477'); #👷
$twisted_rightwards_arrows = [char]::ConvertFromUtf32('0x1f500'); #🔀
$bookmark = [char]::ConvertFromUtf32('0x1f516'); #🔖
$clapper = [char]::ConvertFromUtf32('0x1f3ac'); #🎬
$coffin = [char]::ConvertFromUtf32('0x26b0'); #⚰️
$rewind = [char]::ConvertFromUtf32('0x023ea'); #⏪️

$dicts = @(
  @{ code = ':sparkles:'; emoji = $sparkles; },
  @{ code = ':new:'; emoji = $new; },
  @{ code = ':boom:'; emoji = $boom; },
  @{ code = ':bug:'; emoji = $bug; },
  @{ code = ':speech_balloon:'; emoji = $speech_balloon; },
  @{ code = ':ok_hand:'; emoji = $ok_hand; },
  @{ code = ':lipstick:'; emoji = $lipstick; },
  @{ code = ':art:'; emoji = $art; },
  @{ code = ':white_check_mark:'; emoji = $white_check_mark; },
  @{ code = ':wrench:'; emoji = $wrench; },
  @{ code = ':arrow_up:'; emoji = $arrow_up; },
  @{ code = ':arrow_down:'; emoji = $arrow_down; },
  @{ code = ':heavy_plus_sign:'; emoji = $heavy_plus_sign; },
  @{ code = ':heavy_minus_sign:'; emoji = $heavy_minus_sign; },
  @{ code = ':see_no_evil:'; emoji = $see_no_evil; },
  @{ code = ':pencil2:'; emoji = $pencil; },
  @{ code = ':memo:'; emoji = $memo; },
  @{ code = ':bulb:'; emoji = $bulb; },
  @{ code = ':zap:'; emoji = $zap; },
  @{ code = ':wastebasket:'; emoji = $wastebasket; },
  @{ code = ':fire:'; emoji = $fire; },
  @{ code = ':recycle:'; emoji = $recycle; },
  @{ code = ':truck:'; emoji = $truck; },
  @{ code = ':tada:'; emoji = $tada; },
  @{ code = ':hammer:'; emoji = $hammer; },
  @{ code = ':construction:'; emoji = $construction; },
  @{ code = ':children_crossing:'; emoji = $children_crossing; }
  @{ code = ':construction_worker:'; emoji = $construction_worker; }
  @{ code = ':twisted_rightwards_arrows:'; emoji = $twisted_rightwards_arrows; }
  @{ code = ':bookmark:'; emoji = $bookmark; }
  @{ code = ':clapper:'; emoji = $clapper; }
  @{ code = ':coffin:'; emoji = $coffin; }
  @{ code = ':rewind:'; emoji = $rewind; }
)

$typeOptions = [System.Management.Automation.Host.ChoiceDescription[]] (
  (New-Object System.Management.Automation.Host.ChoiceDescription "&feat", "New features or updates."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "fi&x", "Bug fix."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&docs", "Changes to documentation."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&style", "Formatting, missing semi clons, etc. No production code change."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&refactor", "Refactoring production code."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&test", "Adding missing tests, refactoring tests; no production code change."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&chore", "Updating grunt tasks etc; no production code change."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&notype", "No type.")
)

$branchTypeOptions = [System.Management.Automation.Host.ChoiceDescription[]] (
  (New-Object System.Management.Automation.Host.ChoiceDescription "&feature", "Any code changes for a new module or use case should be done on a feature branch."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "bugfi&x", "Any necessary fixes after a release, sprint or demo should be done on the bugfix branch."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&hotfix", "Hotfix does not follow the scheduled integration of code and could be merged directly to the production branch, then on the development branch later."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&experimental", "Any new feature or idea that is not part of a release or a sprint. A branch for playing around."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&build", "A branch specifically for creating specific build artifacts or for doing code coverage runs."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&release", "A branch for tagging a specific release version."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&merge", "A temporary branch for resolving merge conflicts, usually between the latest development and a feature or Hotfix branch. This can also be used if two branches of a feature being worked on by multiple developers need to be merged, verified and finalized."),
  (New-Object System.Management.Automation.Host.ChoiceDescription "&notype", "No type.")
)

$featOptions = [System.Management.Automation.Host.ChoiceDescription[]] @(
  "$new(New &feat)",
  "$boom(&Break changes)"
  "$tada(Begin a &project)",
  "$construction(&Work in progress)",
  "$children_crossing(&Improve user experience / usability)",
  "(&No emoji)"
)

$fixOptions = [System.Management.Automation.Host.ChoiceDescription[]](
  "$bug(&Fix bugs)",
  "$speech_balloon(Add or update &text and literals)",
  "$pencil2(Fix t&ypos)",
  "(&No emoji)"
)

$styleOptions = [System.Management.Automation.Host.ChoiceDescription[]](
  "$lipstick(Add or update the UI and &style files)",
  "$art(Improve structure / &format of the code)",
  "(&No emoji)"
)

$testOptions = [System.Management.Automation.Host.ChoiceDescription[]](
  "$white_check_mark(Add or update &tests)",
  "(&No emoji)"
)

$choreOptions = [System.Management.Automation.Host.ChoiceDescription[]](
  "$wrench(Add or update configuration &files)",
  "$arrow_up(&Upgrade dependencies.)",
  "$arrow_down(&Downgrade dependencies)",
  "$heavy_plus_sign(&Add a dependency)",
  "$heavy_minus_sign(&Remove a dependency)",
  "$see_no_evil(Add or update a .&gitignore file)",
  "$hammer(Add or update development &scripts)",
  "$construction_worker(Add or update &CI build system)",
  "$twisted_rightwards_arrows(&Merge branches)",
  "$bookmark(Release / Version &tags)",
  "$rewind(Re&vert changes)",
  "(&No emoji)"
)

$docsOptions = [System.Management.Automation.Host.ChoiceDescription[]](
  "$memo(Add or update &documentation)",
  "$bulb(Add or update &comments in source code)",
  "$clapper(Add or update demos and &examples)",
  "(&No emoji)"
)

$refactorOptions = [System.Management.Automation.Host.ChoiceDescription[]](
  "$recycle(&Refactor code)",
  "$zap(Improve &performance)",
  "$truck(&Move or rename resources(e.g.: files, paths, routes))",
  "$fire(Remove code or &files)",
  "$coffin(Remove &dead code)",
  "(&No emoji)"
)

$global:GitUtilsConfig = @{
  Emoji      = $true;
  Type       = $true;
  Scope      = $true;
  EmojiFirst = $false; # determine whether placing the <emoji> in front of <type>
}
$config = $global:GitUtilsConfig

function Invoke-GitAdd {
  if ($args.Count -eq 0) {
    git add .
  }
  else {
    git add $args
  }
}

## git branch [-a|$args]
function Invoke-GitBranch {
  if ($args.Count -eq 0) {
    git branch -av
  }
  else {
    git branch $args
  }
}


## git branch -d
function Invoke-GitBranchDelete {
  if ($args.Count -eq 0) {
    [string]$localBranch = Get-OptionsForChoosingLocalBranch "Delete branch" "Please choose a branch to delete"
    if ($localBranch) {
      Invoke-GitBranchDelete $localBranch
    }
  }
  else {
    for ($i = 0; $i -lt $args.Count; $i++) {
      $branch = $args[$i]

      Write-Host "Running 'git branch -d $branch'..." -ForegroundColor Green

      $result = (git branch -d $branch 2>&1)

      if ($result -isnot [array] -or !$result[1].ToString().Contains("git branch -D $branch")) {
        $result
      }
      else {
        $message = "Do you want to run 'git branch -D $branch'?"
        $yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Yes"
        $no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "No"
        $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
        $choice = $host.ui.PromptForChoice($result[0], $message, $options, 1)
        if ($choice -eq 0) {
          git branch -D $branch
        }
      }
    }
  }
}

## git status
function Invoke-GitStatus { git status $args }

function Get-OptionsForChoosingLocalBranch {
  param (
      [string]$title = 'Choose branch',
      [string]$message = 'Please choose a branch'
  )
  [System.Management.Automation.Host.ChoiceDescription[]]$localBranchOptions = @()
  [string[]]$localBranches = Get-GitLocalBranches
  [char[]]$existChars = @('q')

  for ($i = 0; $i -lt $localBranches.Count; $i++) {
    $localBranch = $localBranches[$i]

    [int]$localBranchCharIndex = -1
    [char[]]$localBranchChars = $localBranch.ToCharArray()
    for ($j = 0; $j -lt $localBranchChars.Count; $j++) {
      $char = $localBranchChars[$j]

      if (!$existChars.Contains($char)) {
        $localBranchCharIndex = $j
        $existChars += $char
        break
      }
    }

    if ($localBranchCharIndex -ne -1) {
      $localBranch = $localBranch.Insert($localBranchCharIndex, '&')
    }

    $localBranchOptions += $localBranch
  }

  $localBranchOptions += "&quit"

  $choice = $host.UI.PromptForChoice($title, $message, $localBranchOptions, $localBranchOptions.Count - 1)

  return $localBranches[$choice]
}

## git checkout [$args]
function Invoke-GitCheckout {
  if ($args.Count -eq 0) {
    $localBranch = Get-OptionsForChoosingLocalBranch "Switch branch" "Please choose a branch to switch"

    if ($localBranch) {
      git checkout $localBranch
    }
  }
  else {
    git checkout $args
  }
}

## list origin branches
function Get-GitOriginBranches {
  param(
    [switch]$onlyName
  )
  return (
    (git branch -r 2>&1) |
    Where-Object { !$_.Contains('HEAD') } |
    ForEach-Object {
      if ($onlyName) {
        $_.Substring(' origin/'.Length)
      }
      else {
        $_.Substring(2)
      }
    }
  )
}

function Get-GitLocalBranches {
  return (git branch 2>&1) | ForEach-Object { $_.Substring(2) }
}

<#
.SYNOPSIS
git checkout -b newBranch startPoint

.DESCRIPTION
Create a new branch and checkout to

.PARAMETER branch
The branch name that you want to named

.EXAMPLE
Invoke-GitCheckoutNewBranch order
#>

function Invoke-GitCheckoutNewBranch {
  param (
    [Parameter(Mandatory = $true, HelpMessage = 'The name of new branch to create.')]
    [string]$branch
  )
  [string]$startPoint

  [int]$step = 1

  $typeIndex = (Get-Host).UI.PromptForChoice("Creating a new branch and checkout to...", "${step}. Please choose a type for the new branch", $branchTypeOptions, 7)

  switch ($typeIndex) {
    0 { $branch = "feature/$branch" }
    1 { $branch = "bugfix/$branch" }
    2 { $branch = "hotfix/$branch" }
    3 { $branch = "experimental/$branch" }
    4 { $branch = "build/$branch" }
    5 { $branch = "release/$branch" }
    6 { $branch = "merge/$branch" }
    Default {}
  }

  Write-Host
  $step++

  [System.Management.Automation.Host.ChoiceDescription[]]$originBranchOptions = @()
  [string[]]$originBranches = Get-GitOriginBranches -onlyName
  [char[]]$existChars = @('n')
  for ($i = 0; $i -lt $originBranches.Count; $i++) {
    $originBranch = $originBranches[$i]

    [int]$originBranchCharIndex = -1
    [char[]]$originBranchChars = $originBranch.ToCharArray()
    for ($j = 0; $j -lt $originBranchChars.Count; $j++) {
      $char = $originBranchChars[$j]

      if (!$existChars.Contains($char)) {
        $originBranchCharIndex = $j
        $existChars += $char
        break
      }
    }

    if ($originBranchCharIndex -ne -1) {
      $originBranch = $originBranch.Insert($originBranchCharIndex, '&')
    }

    $item = 'origin/' + $originBranch
    $originBranchOptions += $item
  }
  $originBranchOptions += "&notrack";

  $originBranchIndex = (Get-Host).UI.PromptForChoice("", "${step}. Please choose a remote branch to track", $originBranchOptions, $originBranchOptions.Count - 1)

  if ($originBranchIndex -lt $originBranches.Count) {
    $startPoint = "origin/$($originBranches[$originBranchIndex])"
  }

  git checkout -b $branch $startPoint
}

## git pull
function Invoke-GitPull { git pull $args }

## git push
function Invoke-GitPush { git push $args }

## git reset
function Invoke-GitReset { git reset $args }

## git diff
function Invoke-GitDiff { git diff $args }

<#
.SYNOPSIS
Try to delete the local branches that no longer exist on the remote

.DESCRIPTION
Remove any remote-tracking references that no longer exist on the remote,
and then try to delete the local branches that no longer exist on the remote
#>

function Remove-LocalBranchesThatNoLongerExistOnRemote {
  $(git fetch -p 2>&1) |
  Where-Object { $_ -match 'origin\/[a-zA-Z-*]+\/[a-zA-Z-*]+' } |
  ForEach-Object { ($_ -split 'origin/')[1] } |
  ForEach-Object { Invoke-GitBranchDelete $_ }
}

<#
.EXAMPLE
Format-GitCommitMessage 'Initial commit'
#>

function Format-GitCommitMessage {
  param(
    [Parameter(Mandatory = $true)]
    [string]$message
  )
  [string]$type
  [string]$emoji
  [string]$scope

  [int]$step = 1

  if ($config.Emoji -or $config.Type) {
    $typeIndex = (Get-Host).UI.PromptForChoice("Committing messages...", "${step}. Please choose a type for this changes that you commit:", $typeOptions, 7)
    $step++

    Write-Host
    $choiceMessage = "${step}. Please choose an emoji:"
    $step++

    switch ($typeIndex) {
      0 {
        if ($config.Emoji) {
          $featIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $featOptions, 0)
          switch ($featIndex) {
            0 { $emoji = $new }
            1 { $emoji = $boom }
            2 { $emoji = $tada }
            3 { $emoji = $construction }
            4 { $emoji = $children_crossing }
          }
        }

        $type = "feat"
      }
      1 {
        if ($config.Emoji) {
          $fixIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $fixOptions, 0)
          switch ($fixIndex) {
            0 { $emoji = $bug }
            1 { $emoji = $speech_balloon }
            2 { $emoji = $pencil2 }
          }
        }

        $type = "fix"
      }
      2 {
        if ($config.Emoji) {
          $docsIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $docsOptions, 0)
          switch ($docsIndex) {
            0 { $emoji = $memo }
            1 { $emoji = $bulb }
            2 { $emoji = $clapper }
          }
        }

        $type = "docs"
      }
      3 {
        if ($config.Emoji) {
          $styleIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $styleOptions, 0)
          switch ($styleIndex) {
            0 { $emoji = $lipstick }
            1 { $emoji = $art }
          }
        }

        $type = "style"
      }
      4 {
        if ($config.Emoji) {
          $refactorIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $refactorOptions, 0)
          switch ($refactorIndex) {
            0 { $emoji = $recycle }
            1 { $emoji = $zap }
            2 { $emoji = $truck }
            3 { $emoji = $coffin }
          }
        }

        $type = "refactor"
      }
      5 {
        if ($config.Emoji) {
          $testIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $testOptions, 0)
          switch ($testIndex) {
            0 { $emoji = $white_check_mark }
          }
        }

        $type = "test"
      }
      6 {
        if ($config.Emoji) {
          $choreIndex = (Get-Host).UI.PromptForChoice("", $choiceMessage, $choreOptions, 0)
          switch ($choreIndex) {
            0 { $emoji = $wrench }
            1 { $emoji = $arrow_up }
            2 { $emoji = $arrow_down }
            3 { $emoji = $heavy_plus_sign }
            4 { $emoji = $heavy_minus_sign }
            5 { $emoji = $see_no_evil }
            6 { $emoji = $hammer }
            7 { $emoji = $construction_worker }
            8 { $emoji = $twisted_rightwards_arrows }
            9 { $emoji = $bookmark }
            10 { $emoji = $rewind }
          }
        }

        $type = "chore"
      }
    }
  }

  if ($config.Scope) {
    Write-Host
    Write-Host "${step}. Please input the scope about this commit, or press the Enter key to skip:"
    $scope = Read-Host
  }

  Write-Host

  [string]$newMessage

  if ($config.EmojiFirst) {
    if ($config.Emoji -and ![string]::IsNullOrEmpty($emoji)) {
      $newMessage += $emoji + " "
    }

    if ($config.Type) {
      $newMessage += $type
    }
  }
  else {
    if ($config.Type) {
      $newMessage += $type
    }

    if ($config.Scope -and ![string]::IsNullOrEmpty($scope)) {
      $newMessage += "(" + $scope + ")"
    }
  }

  if ($config.Scope -and ![string]::IsNullOrEmpty($scope)) {
    $newMessage += "(" + $scope + ")"
  }

  if (![string]::IsNullOrEmpty($newMessage)) {
    $newMessage += ": "
  }

  return $newMessage + $message
}

<#
.EXAMPLE
Invoke-GitCommit 'Initial commit'
#>

function Invoke-GitCommit {
  param (
    [Parameter(Mandatory = $true, HelpMessage = 'The commit message for git.')]
    [Alias('m')]
    [string]$message,
    # --no-verify
    [switch]$noVerify
  )

  try {
    $null = Get-Command git -ErrorAction stop
  }
  catch {
    Write-Error 'Could not find Git, please install Git first.'
    return
  }

  [System.Collections.ArrayList]$params = @()
  if ($noVerify) {
    $params.Add("--no-verify")
  }

  $newMessage = Format-GitCommitMessage $message

  git commit -m $newMessage $params
}

<#
.EXAMPLE
Invoke-Emojify ':boom:'
#>

function Invoke-Emojify {
  [CmdletBinding()]
  param (
    [Parameter(
      Position = 0,
      Mandatory = $False,
      ValueFromPipeline = $True,
      ValueFromPipelineByPropertyName = $True
    )]
    [string[]]
    $messages
  )

  process {
    foreach ($message in $messages) {
      if ([string]::IsNullOrEmpty($message)) {
        continue;
      }

      $regex = '\:[a-z]\w*(\d|[a-z])*\:'

      $values = ($message | Select-String $regex -AllMatches).Matches.Value

      foreach ($item in $values) {
        $dict = $dicts.Where( { $_.code -eq $item }, 'First')
        $message = $message -replace ($item, $dict.emoji)
      }

      $message
    }
  }
}

<#
.EXAMPLE
Invoke-GitHistory
Invoke-GitHistory 20
#>

function Invoke-GitHistory {
  param (
    [Parameter(HelpMessage = 'The number of logs need to show.')]
    [Alias('c')]
    [int]$count = 10
  )

  try {
    $null = Get-Command git -ErrorAction stop
  }
  catch {
    Write-Error 'Could not find Git, please install Git first.'
    return
  }

  $arg = '-' + $count.ToString()

  [string[]]$logs = git log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit $arg

  Invoke-Emojify($logs)
}

Set-Alias gga Invoke-GitAdd
Set-Alias ggc Invoke-GitCommit
Set-Alias ggb Invoke-GitBranch
Set-Alias ggbd Invoke-GitBranchDelete
Set-Alias ggs Invoke-GitStatus
Set-Alias ggck Invoke-GitCheckout
Set-Alias ggckb Invoke-GitCheckoutNewBranch
Set-Alias ggpl Invoke-GitPull
Set-Alias ggps Invoke-GitPush
Set-Alias ggrst Invoke-GitReset
Set-Alias ggd Invoke-GitDiff
Set-Alias ggl Invoke-GitHistory
Set-Alias emojify Invoke-Emojify
Set-Alias ggbs Remove-LocalBranchesThatNoLongerExistOnRemote

Export-ModuleMember -Function Invoke-GitCommit, Format-GitCommitMessage, Invoke-GitHistory, Invoke-Emojify, Invoke-GitAdd, Invoke-GitBranch, Invoke-GitBranchDelete, Invoke-GitStatus, Invoke-GitCheckout, Get-GitOriginBranches, Invoke-GitCheckoutNewBranch, Invoke-GitPull, Invoke-GitPush, Invoke-GitReset, Invoke-GitDiff, Remove-LocalBranchesThatNoLongerExistOnRemote
Export-ModuleMember -Alias  ggc, ggl, emojify, gga, ggb, ggbd, ggs, ggck, ggckb, ggpl, ggps, ggrst, ggd, ggbs
Export-ModuleMember -Variable $global:GitUtilsConfig