#!/usr/bin/env bash

GITSECRET_VERSION='0.2.2'  # shellcheck disable=2034
#!/usr/bin/env bash


function __replace_in_file_linux {
  sed -i.bak "s/^\($1\s*=\s*\).*\$/\1$2/" "$3"
}


function __temp_file_linux {
  local filename
  filename=$(mktemp)
  echo "$filename"
}
#!/usr/bin/env bash


function __replace_in_file_osx {
  sed -i.bak "s/^\($1[[:space:]]*=[[:space:]]*\).*\$/\1$2/" "$3"
}


function __temp_file_osx {
  : "${TMPDIR:=/tmp}"
  local filename
  filename=$(mktemp -t _gitsecrets_XXX )
  echo "$filename";
}
#!/usr/bin/env bash

# Global variables:
WORKING_DIRECTORY="$PWD"  # shellcheck disable=2034

# Folders:
SECRETS_DIR=".gitsecret"
SECRETS_DIR_KEYS="$SECRETS_DIR/keys"
SECRETS_DIR_PATHS="$SECRETS_DIR/paths"

# Files:
SECRETS_DIR_KEYS_MAPPING="$SECRETS_DIR_KEYS/mapping.cfg"  # shellcheck disable=2034
SECRETS_DIR_KEYS_TRUSTDB="$SECRETS_DIR_KEYS/trustdb.gpg"  # shellcheck disable=2034

SECRETS_DIR_PATHS_MAPPING="$SECRETS_DIR_PATHS/mapping.cfg"  # shellcheck disable=2034

: "${SECRETS_EXTENSION:=".secret"}"

# Commands:
: "${SECRETS_GPG_COMMAND:="gpg"}"
GPGLOCAL="$SECRETS_GPG_COMMAND --homedir=$SECRETS_DIR_KEYS --no-permission-warning"


# Inner bash:

function _function_exists {
  declare -f -F "$1" > /dev/null 2>&1
  echo $?
}


# OS based:

function _os_based {
  # Pass function name as first parameter.
  # It will be invoked as os-based function with the postfix.

  case "$(uname -s)" in

    Darwin)
      "$1_osx" "${@:2}"
    ;;

    Linux)
      "$1_linux" "${@:2}"
    ;;

    # TODO: add MS Windows support.
    # CYGWIN*|MINGW32*|MSYS*)
    #   $1_ms ${@:2}
    # ;;

    *)
      _abort 'unsupported OS.'
    ;;
  esac
}


# File System:

function _set_config {
  # First parameter is the KEY, second is VALUE, third is filename.

  # The exit status is 0 (true) if the name was found, 1 (false) if not:
  local contains
  contains=$(grep -Fq "$1" "$3"; echo "$?")

  if [[ "$contains" -eq 0 ]]; then
    _os_based __replace_in_file "$@"
  elif [[ "$contains" -eq 1 ]]; then
    echo "$1 = $2" >> "$3"
  fi
}


function _file_has_line {
  # First parameter is the KEY, second is the filename.

  local contains
  contains=$(grep -qw "$1" "$2"; echo $?)
  # 0 on contains, 1 for error.
  echo "$contains";
}


function _delete_line {
  local escaped_path
  escaped_path=$(echo "$1" | sed -e 's/[\/&]/\\&/g')
  sed -i.bak "/$escaped_path/d" "$2"
}


function _temporary_file {
  # This function creates temporary file
  # which will be removed on system exit.
  filename=$(_os_based __temp_file)  # is not `local` on purpose.

  trap 'echo "cleaning up..."; rm -f "$filename";' EXIT
}


function _unique_filename {
  # First parameter is base-path, second is filename,
  # third is optional extension.
  local n=0 result=$2
  while true; do
    if [[ ! -f "$1/$result" ]]; then
      break
    fi

    n=$(( n + 1 ))
    result="${2}-${n}"
  done
  echo "$result"
}


# Manuals:

function _show_manual_for {
  local function_name="$1"
  man "git-secret-${function_name}"
  exit 0
}


# VCS:

function _check_ignore {
  git check-ignore --no-index -q "$1";
  echo $?
}


function _add_ignored_file {
  if [[ ! -f ".gitignore" ]]; then
    touch ".gitignore"
  fi

  echo "$1" >> ".gitignore"
}


function _is_inside_git_tree {
  git rev-parse --is-inside-work-tree >/dev/null 2>&1
  echo $?
}


function _get_git_root_path {
  # We need this function to get the location of the `.git` folder,
  # since `.gitsecret` must be on the same level.
  local result
  result=$(git rev-parse --show-toplevel)
  echo "$result"
}


# Logic:

function _abort {
  >&2 echo "$1 abort."
  exit 1
}

function _find_and_clean {
  # required:
  local pattern="$1" # can be any string pattern

  # optional:
  local verbose=${2:-""} # can be empty or should be equal to "v"

  # shellcheck disable=2086
  find . -name "$pattern" -type f -print0 | xargs -0 rm -f$verbose
}


function _find_and_clean_formated {
  # required:
  local pattern="$1" # can be any string pattern

  # optional:
  local verbose=${2:-""} # can be empty or should be equal to "v"
  local message=${3:-"cleaning:"} # can be any string

  if [[ ! -z "$verbose" ]]; then
    echo && echo "$message"
  fi

  _find_and_clean "$pattern" "$verbose"

  if [[ ! -z "$verbose" ]]; then
    echo
  fi
}


function _secrets_dir_exists {
  local root_path
  root_path=$(_get_git_root_path)

  local full_path="$root_path/$SECRETS_DIR"

  if [[ ! -d "$full_path" ]]; then
    _abort "$full_path does not exist."
  fi
}


function _user_required {
  _secrets_dir_exists

  local error_message="no users found. run 'git secret tell' before adding files."
  if [[ ! -f "$SECRETS_DIR_KEYS_TRUSTDB" ]]; then
    _abort "$error_message"
  fi

  local keys_exist
  keys_exist=$($GPGLOCAL -n --list-keys)
  if [[ -z "$keys_exist" ]]; then
    _abort "$error_message"
  fi
}


function _get_raw_filename {
  echo "$(dirname "$1")/$(basename "$1" "$SECRETS_EXTENSION")" | sed -e 's#^\./##'
}


function _get_encrypted_filename {
  local filename
  filename="$(dirname "$1")/$(basename "$1" "$SECRETS_EXTENSION")"
  echo "${filename}${SECRETS_EXTENSION}" | sed -e 's#^\./##'
}


function _get_users_in_keyring {
  local result
  result=$($GPGLOCAL --list-public-keys --with-colon | sed -n 's/.*<\(.*\)>.*/\1/p')
  echo "$result"
}


function _get_recepients {
  local result
  result=$($GPGLOCAL --list-public-keys --with-colon | sed -n 's/.*<\(.*\)>.*/-r\1/p')
  echo "$result"
}


function _decrypt {
  # required:
  local filename="$1"

  # optional:
  local write_to_file=${2:-1} # can be 0 or 1
  local force=${3:-0} # can be 0 or 1
  local homedir=${4:-""}
  local passphrase=${5:-""}

  local encrypted_filename
  encrypted_filename=$(_get_encrypted_filename "$filename")

  local base="$SECRETS_GPG_COMMAND --use-agent -q --decrypt --no-permission-warning"

  if [[ "$write_to_file" -eq 1 ]]; then
    base="$base -o $filename"
  fi

  if [[ "$force" -eq 1 ]]; then
    base="$base --yes"
  fi

  if [[ ! -z "$homedir" ]]; then
    base="$base --homedir=$homedir"
  fi

  if [[ ! -z "$passphrase" ]]; then
    echo "$passphrase" | $base --batch --yes --no-tty --passphrase-fd 0 \
      "$encrypted_filename" > /dev/null 2>&1
  else
    $base "$encrypted_filename" > /dev/null 2>&1
  fi
}
#!/usr/bin/env bash


function reveal {
  local homedir=''
  local passphrase=''
  local force=0

  OPTIND=1

  while getopts 'hfd:p:' opt; do
    case "$opt" in
      h) _show_manual_for 'reveal';;

      f) force=1;;

      p) passphrase=$OPTARG;;

      d) homedir=$OPTARG;;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  _user_required

  local counter=0
  while read -r line; do
    # The parameters are: filename, write-to-file, force, homedir, passphrase
    _decrypt "$line" "1" "$force" "$homedir" "$passphrase"

    counter=$((counter+1))
  done < "$SECRETS_DIR_PATHS_MAPPING"

  echo "done. all $counter files are revealed."
}
#!/usr/bin/env bash


function add {
  local auto_add=0
  OPTIND=1

  while getopts "ih" opt; do
    case "$opt" in
      i) auto_add=1;;

      h) _show_manual_for "add";;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = "--" ] && shift

  _user_required

  local not_ignored=()
  local items=( "$@" )

  for item in "${items[@]}"; do
    # Checking if all files in options are ignored:
    if [[ ! -f "$item" ]]; then
      _abort "$item is not a file."
    fi

    local ignored
    ignored=$(_check_ignore "$item")
    if [[ ! "$ignored" -eq 0 ]]; then
      # Collect unignored files.
      not_ignored+=("$item")
    fi
  done

  if [[ ! "${#not_ignored[@]}" -eq 0 ]]; then
    # And show them all at once.
    local message
    message="these files are not ignored: $* ;"

    if [[ "$auto_add" -eq 0 ]]; then
      # This file is not ignored. user don't want it to be added automatically.
      # Raise the exception, since all files, which will be hidden, must be ignored.
      _abort "$message"
    else
      # In this case these files should be added to the `.gitignore` automatically:
      # see https://github.com/sobolevn/git-secret/issues/18 for more.
      echo "$message"
      echo "auto adding them to .gitignore"
      for item in "${not_ignored[@]}"; do
        _add_ignored_file "$item"
      done
    fi
  fi

  for item in "${items[@]}"; do
    # Adding files into system, skipping duplicates.
    local already_in
    already_in=$(_file_has_line "$item" "$SECRETS_DIR_PATHS_MAPPING")
    if [[ "$already_in" -eq 1 ]]; then
      echo "$item" >> "$SECRETS_DIR_PATHS_MAPPING"
    fi
  done

  echo "${#@} items added."
}
#!/usr/bin/env bash


function _optional_clean {
  local clean="$1"
  local verbose=${2:-""}

  if [[ $clean -eq 1 ]]; then
    _find_and_clean_formated "*$SECRETS_EXTENSION" "$verbose"
  fi
}


function _optional_delete {
  local delete="$1"
  local verbose=${2:-""}

  if [[ $delete -eq 1 ]]; then
    # We use custom formating here:
    if [[ ! -z "$verbose" ]]; then
      echo && echo 'removing unencrypted files:'
    fi

    while read -r line; do
      # So the formating would not be repeated several times here:
      _find_and_clean "*$line" "$verbose"
    done < "$SECRETS_DIR_PATHS_MAPPING"

    if [[ ! -z "$verbose" ]]; then
      echo
    fi
  fi

}


function hide {
  local clean=0
  local delete=0
  local verbose=''

  OPTIND=1

  while getopts 'cdvh' opt; do
    case "$opt" in
      c) clean=1;;

      d) delete=1;;

      v) verbose='v';;

      h) _show_manual_for 'hide';;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  # We need user to continue:
  _user_required

  # If -c option was provided, it would clean the hidden files
  # before creating new ones.
  _optional_clean "$clean" "$verbose"

  local counter=0
  while read -r line; do
    local encrypted_filename
    encrypted_filename=$(_get_encrypted_filename "$line")

    local recipients
    recipients=$(_get_recepients)

    # shellcheck disable=2086
    $GPGLOCAL --use-agent --yes --trust-model=always --encrypt \
      $recipients -o "$encrypted_filename" "$line"

    counter=$((counter+1))
  done < "$SECRETS_DIR_PATHS_MAPPING"

  # If -d option was provided, it would delete the source files
  # after we have already hidden them.
  _optional_delete "$delete" "$verbose"

  echo "done. all $counter files are hidden."
}
#!/usr/bin/env bash


function whoknows {
  OPTIND=1

  while getopts "h?" opt; do
    case "$opt" in
      h) _show_manual_for "whoknows";;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = "--" ] && shift

  # Validating, that we have a user:
  _user_required

  local keys

  # Getting the users from gpg:
  keys=$(_get_users_in_keyring)
  echo "$keys"
}
#!/usr/bin/env bash


function remove {
  local clean=0

  OPTIND=1

  while getopts 'ch' opt; do
    case "$opt" in
      c) clean=1;;

      h) _show_manual_for 'remove';;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  # Validate if user exists:
  _user_required

  for item in "$@"; do
    if [[ ! -f "$item" ]]; then
      _abort "$item is not a file."
    fi

    _delete_line "$item" "$SECRETS_DIR_PATHS_MAPPING"
    rm -f "${SECRETS_DIR_PATHS_MAPPING}.bak"  # not all systems create '.bak'

    if [[ "$clean" == 1 ]]; then
      local encrypted_filename
      encrypted_filename=$(_get_encrypted_filename "$item")

      rm -f "$encrypted_filename"
    fi

  done

  echo 'removed from index.'
  echo "ensure that files: [$*] are now not ignored."
}
#!/usr/bin/env bash


function init {
  OPTIND=1

  while getopts 'h' opt; do
    case "$opt" in
      h) _show_manual_for 'init';;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  if [[ -d "$SECRETS_DIR" ]]; then
    _abort 'already inited.'
  fi

  local ignores
  ignores=$(_check_ignore "$SECRETS_DIR"/)

  if [[ ! $ignores -eq 1 ]]; then
    _abort "'${SECRETS_DIR}/' is ignored."
  fi

  mkdir "$SECRETS_DIR" "$SECRETS_DIR_KEYS" "$SECRETS_DIR_PATHS"
  touch "$SECRETS_DIR_KEYS_MAPPING" "$SECRETS_DIR_PATHS_MAPPING"

  echo "'${SECRETS_DIR}/' created."
}
#!/usr/bin/env bash


function clean {
  local verbose=''

  OPTIND=1

  while getopts 'vh' opt; do
    case "$opt" in
      v) verbose="v";;

      h) _show_manual_for 'clean';;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  _user_required

  # User should see properly formated output:
  _find_and_clean_formated "*$SECRETS_EXTENSION" "$verbose"
}
#!/usr/bin/env bash


function killperson {
  OPTIND=1

  while getopts 'h' opt; do
    case "$opt" in
      h) _show_manual_for 'killperson';;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = "--" ] && shift

  _user_required

  local emails=( "$@" )

  if [[ ${#emails[@]} -eq 0 ]]; then
    _abort "at least one email is required."
  fi

  for email in "${emails[@]}"; do
    $GPGLOCAL --batch --yes --delete-key "$email"
  done
}
#!/usr/bin/env bash


function usage {
  OPTIND=1

  while getopts "h?" opt; do
    case "$opt" in
      h) _show_manual_for "usage";;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = "--" ] && shift

  # There was a bug with some shells, which were adding extra commands
  # to the old dynamic-loading version of this code.
  # thanks to @antmak it is now fixed, see:
  # https://github.com/sobolevn/git-secret/issues/47
  local commands="add|changes|clean|hide|init|killperson|list|remove|reveal|tell|usage|whoknows"

  echo "usage: git secret [$commands]"
}
#!/usr/bin/env bash

function changes {
  local passphrase=""

  OPTIND=1

  while getopts 'hd:p:' opt; do
    case "$opt" in
      h) _show_manual_for 'changes';;

      p) passphrase=$OPTARG;;

      d) homedir=$OPTARG;;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  _user_required

  local filenames="$*"
  if [[ -z $filenames ]]; then
    # Checking if no filenames are passed, show diff for all files.
    filenames=$(git secret list)
  fi

  IFS='
  '

  for filename in $filenames; do
    local decrypted
    local content
    local diff_result

    # Now we have all the data required:
    decrypted=$(_decrypt "$filename" "0" "0" "$homedir" "$passphrase")
    content=$(cat "$filename")

    # Let's diff the result:
    diff_result=$(diff <(echo "$decrypted") <(echo "$content")) || true
    # There was a bug in the previous version, since `diff` returns
    # exit code `1` when the files are different.
    echo "changes in ${filename}: ${diff_result}"
  done
}
#!/usr/bin/env bash


function tell {
  local emails
  local self_email=0
  local homedir

  # A POSIX variable
  # Reset in case getopts has been used previously in the shell.
  OPTIND=1

  while getopts "hmd:" opt; do
    case "$opt" in
      h) _show_manual_for "tell";;

      m) self_email=1;;

      d) homedir=$OPTARG;;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = "--" ] && shift

  # Validates that application is inited:
  _secrets_dir_exists

  emails=( "$@" )
  local git_email

  if [[ "$self_email" -eq 1 ]]; then
    git_email=$(git config user.email)

    if [[ -z "$git_email" ]]; then
      _abort "'git config user.email' is not set."
    fi

    emails+=("$git_email")
  fi

  if [[ "${#emails[@]}" -eq 0 ]]; then
    # If after possible addition of git_email, emails are still empty,
    # we should raise an exception.
    _abort "you must provide at least one email address."
  fi

  for email in "${emails[@]}"; do
    # This file will be removed automatically:
    _temporary_file  # note, that `_temporary_file` will export `filename` var.
    # shellcheck disable=2154
    local keyfile="$filename"

    if [[ -z "$homedir" ]]; then
      $SECRETS_GPG_COMMAND --export -a "$email" > "$keyfile"
    else
      # It means that homedir is set as an extra argument via `-d`:
      $SECRETS_GPG_COMMAND --no-permission-warning --homedir="$homedir" \
        --export -a "$email" > "$keyfile"
    fi

    if [[ ! -s "$keyfile" ]]; then
      _abort 'gpg key is empty. check your key name: "gpg --list-keys".'
    fi

    # Importing public key to the local keychain:
    $GPGLOCAL --import "$keyfile" > /dev/null 2>&1
  done

  echo "done. ${emails[*]} added as someone who know(s) the secret."
}
#!/usr/bin/env bash


function list {
  OPTIND=1

  while getopts 'h' opt; do
    case "$opt" in
      h) _show_manual_for 'list';;
    esac
  done

  shift $((OPTIND-1))
  [ "$1" = '--' ] && shift

  _user_required

  if [[ ! -s "$SECRETS_DIR_PATHS_MAPPING" ]]; then
    _abort "$SECRETS_DIR_PATHS_MAPPING is missing."
  fi

  while read -r line; do
    echo "$line"
  done < "$SECRETS_DIR_PATHS_MAPPING"
}
#!/usr/bin/env bash

set -e

function _check_setup {
  # Checking git and secret-plugin setup:
  local is_tree
  is_tree=$(_is_inside_git_tree)
  if [[ ! $is_tree -eq 0 ]]; then
    _abort "repository is broken. try running 'git init' or 'git clone'."
  fi

  # Checking if the '.gitsecret' is not ignored:
  local ignored
  ignored=$(_check_ignore ".gitsecret/")
  if [[ ! $ignored -eq 1 ]]; then
    _abort '.gitsecret folder is ignored.'
  fi

  # Checking gpg setup:
  local secring="$SECRETS_DIR_KEYS/secring.gpg"
  if [[ -f $secring ]] && [[ -s $secring ]]; then
    # secring.gpg is not empty, someone has imported a private key.
    _abort 'it seems that someone has imported a secret key.'
  fi
}


function _incorrect_usage {
  echo "$1"
  usage
  exit "$2"
}


function _show_version {
  echo "$GITSECRET_VERSION"
  exit 0
}


function _init_script {
  if [[ $# == 0 ]]; then
    _incorrect_usage 'no input parameters provided.' 126
  fi

  # Parse plugin-level options:
  local dry_run=0

  while [[ $# -gt 0 ]]; do
    local opt="$1"

    case "$opt" in
      # Options for quick-exit strategy:
      --dry-run)
        dry_run=1
        shift;;

      --version) _show_version;;

      *) break;;  # do nothing
    esac
  done

  if [[ "$dry_run" == 0 ]]; then
    # Checking for proper set-up:
    _check_setup

    # Routing the input command:
    if [[ $(_function_exists "$1") == 0 ]] && [[ ! $1 == _* ]]; then
      $1 "${@:2}"
    else  # TODO: elif [[ $(_plugin_exists $1) == 0 ]]; then
      _incorrect_usage "command $1 not found." 126
    fi
  fi
}


_init_script "$@"
