├── .gitignore ├── .github └── workflows │ └── actions.yml ├── test ├── Dockerfile ├── test.zsh └── zshrc ├── LICENSE ├── README.md └── history-sync.plugin.zsh /.gitignore: -------------------------------------------------------------------------------- 1 | zsh_history_test* 2 | .*.swp 3 | *.swp 4 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: history-sync 2 | env: 3 | ACCESS_KEY: ${{ secrets.ACCESS_KEY }} 4 | on: [push, workflow_dispatch] 5 | jobs: 6 | setup_and_test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - run: docker build . --file test/Dockerfile --build-arg ACCESS_KEY=${ACCESS_KEY} --tag test/history-sync:latest 11 | - run: docker run test/history-sync:latest; exit $? 12 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | # Install system packages 4 | RUN apt-get update \ 5 | && apt-get install -y --no-install-recommends \ 6 | ca-certificates \ 7 | curl \ 8 | git \ 9 | zsh \ 10 | gpg \ 11 | gpg-agent \ 12 | ripgrep \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Install oh-my-zsh and history-sync plugin 16 | RUN bash -c 'set -o pipefail && curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh | sh -e' \ 17 | && mkdir -p /root/.oh-my-zsh/plugins/history-sync 18 | COPY history-sync.plugin.zsh /root/.oh-my-zsh/plugins/history-sync/ 19 | # Install a basic `zshrc` 20 | COPY test/zshrc /root/.zshrc 21 | # Install the test script 22 | COPY test/test.zsh /root 23 | RUN chmod +x /root/test.zsh 24 | 25 | ARG ACCESS_KEY 26 | ENV ACCESS_KEY=${ACCESS_KEY} 27 | ENTRYPOINT ["/usr/bin/zsh"] 28 | CMD ["-i", "-c", "source /root/test.zsh ${ACCESS_KEY}"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 James Fraser (https://www.wulfgar.pro) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![history-sync](https://github.com/wulfgarpro/history-sync/actions/workflows/actions.yml/badge.svg)](https://github.com/wulfgarpro/history-sync/actions/workflows/actions.yml) 2 | 3 | # history-sync 4 | 5 | > An Oh My Zsh plugin for GPG encrypted, Internet synchronized Zsh history using Git. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | sudo apt install gpg git 11 | git clone git@github.com:wulfgarpro/history-sync.git 12 | cp -r history-sync ~/.oh-my-zsh/plugins 13 | ``` 14 | 15 | Open `.zshrc` file and add `history-sync` to the plugins list: 16 | 17 | ```bash 18 | plugins=(... history-sync) 19 | ``` 20 | 21 | The reaload Zsh: 22 | 23 | ```bash 24 | exec zsh 25 | ``` 26 | 27 | ## Usage 28 | 29 | Before using `history-sync`, ensure you have: 30 | 31 | 1. A hosted Git repository, e.g. GitHub, Bitbucket, with SSH key access. 32 | 2. A configured GPG key pair for encrypting/decrypting your history file, and the public keys of all 33 | nodes in your web-of-trust. 34 | * See [the GnuPG documentation](https://www.gnupg.org/documentation/) for details. 35 | 36 | Once set up, configure the following environment variables: 37 | 38 | * `ZSH_HISTORY_FILE`: your `zsh_history` file location 39 | * `ZSH_HISTORY_PROJ`: Git project for storing `zsh_history` 40 | * `ZSH_HISTORY_FILE_ENC`: encrypted history file location 41 | * `ZSH_HISTORY_COMMIT_MSG`: default commit message when pushing 42 | * `ZSH_HISTORY_DEFAULT_RECIPIENT`: default recipient for `zhps` 43 | 44 | Defaults: 45 | 46 | ```bash 47 | ZSH_HISTORY_FILE_NAME=".zsh_history" 48 | ZSH_HISTORY_FILE="${HOME}/${ZSH_HISTORY_FILE_NAME}" 49 | ZSH_HISTORY_PROJ="${HOME}/.zsh_history_proj" 50 | ZSH_HISTORY_FILE_ENC_NAME="zsh_history" 51 | ZSH_HISTORY_FILE_ENC="${ZSH_HISTORY_PROJ}/${ZSH_HISTORY_FILE_ENC_NAME}" 52 | ZSH_HISTORY_COMMIT_MSG="latest $(date)" 53 | ZSH_HISTORY_DEFAULT_RECIPIENT="" 54 | ``` 55 | 56 | Optional: 57 | 58 | * `ZSH_HISTORY_GIT_REMOTE`: if set, the plugin clones the remote repo on first use 59 | 60 | ## Commands 61 | 62 | ```bash 63 | # Pull history 64 | zhpl 65 | 66 | # Push history 67 | zhps -r "John Brown" -r 876T3F78 -r ... 68 | 69 | # Or set a default recipient to omit -r: 70 | # ZSH_HISTORY_DEFAULT_RECIPIENT="John Brown" 71 | 72 | # Pull + push history 73 | zhsync 74 | ``` 75 | 76 | ## Demo 77 | 78 | Check out the [screen cast](https://asciinema.org/a/43575). 79 | 80 | ## Licence 81 | 82 | MIT @ [James Fraser](https://www.wulfgar.pro) 83 | -------------------------------------------------------------------------------- /test/test.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | ACCESS_KEY=$1 4 | 5 | set -e 6 | gpg -q --quick-gen-key --yes --batch --passphrase '' $UID 7 | git config --global user.name "James Fraser" 8 | git config --global user.email "wulfgar.pro@gmail.com" 9 | git clone "https://$ACCESS_KEY@github.com/wulfgarpro/history-sync-test" ~/.zsh_history_proj 10 | set +e 11 | 12 | function success() { 13 | echo $fg[green]"$1"$reset_color 14 | } 15 | 16 | function failure() { 17 | echo $fg[red]"$1"$reset_color 18 | exit 1 19 | } 20 | 21 | function info() { 22 | echo $fg[yellow]"$1"$reset_color 23 | } 24 | 25 | function check_fn_exists() { 26 | typeset -f "$1" >/dev/null 27 | [[ $? -eq 0 ]] || {failure "FAILURE: Function '$1' missing"} 28 | } 29 | 30 | function check_alias_exists() { 31 | alias "$1" >/dev/null 32 | [[ $? -eq 0 ]] || {failure "FAILURE: Alias '$1' missing"} 33 | } 34 | 35 | function check_env_exists() { 36 | [[ -v $1 ]] 37 | [[ $? -eq 0 ]] || {failure "FAILURE: Environment variable '$1' missing"} 38 | } 39 | 40 | function check_history() { 41 | cat ~/.zsh_history 42 | rg -U "$1" ~/.zsh_history >/dev/null 43 | [[ $? -eq 0 ]] || {failure "FAILURE: History did not match '$1'"} 44 | } 45 | 46 | function setup() { 47 | set -e 48 | [[ -d ~/.zsh_history_proj ]] 49 | # Clear existing history file 50 | echo -n "" > ~/.zsh_history_proj/zsh_history 51 | set +e 52 | } 53 | 54 | info "TEST HISTORY-SYNC FUNCTIONS EXIST" 55 | check_fn_exists _print_git_error_msg 56 | check_fn_exists _print_gpg_encrypt_error_msg 57 | check_fn_exists _print_gpg_decrypt_error_msg 58 | check_fn_exists _usage 59 | check_fn_exists history_sync_pull 60 | check_fn_exists history_sync_push 61 | success "SUCCESS" 62 | 63 | info "TEST HISTORY-SYNC ALIASES EXIST" 64 | check_alias_exists zhps 65 | check_alias_exists zhpl 66 | check_alias_exists zhsync 67 | success "SUCCESS" 68 | 69 | info "TEST ENVIRONMENT VARIABLES EXIST" 70 | check_env_exists ZSH_HISTORY_PROJ 71 | check_env_exists ZSH_HISTORY_FILE_NAME 72 | check_env_exists ZSH_HISTORY_FILE 73 | check_env_exists ZSH_HISTORY_FILE_ENC_NAME 74 | check_env_exists ZSH_HISTORY_FILE_ENC 75 | check_env_exists ZSH_HISTORY_FILE_DECRYPT_NAME 76 | check_env_exists ZSH_HISTORY_COMMIT_MSG 77 | success "SUCCESS" 78 | 79 | info "TEST SYNC HISTORY BASIC 0" 80 | setup 81 | RAND=$RANDOM 82 | echo "1 echo $RAND" >> ~/.zsh_history 83 | zhps -y -r $UID && zhpl -y 84 | check_history "^1 echo $RAND$" 85 | success "SUCCESS" 86 | 87 | info "TEST SYNC HISTORY BASIC 1" 88 | setup 89 | RAND0=$RANDOM 90 | RAND1=$RANDOM 91 | RAND2=$RANDOM 92 | RAND3=$RANDOM 93 | RAND4=$RANDOM 94 | echo "1 echo $RAND0" >> ~/.zsh_history 95 | echo "2 echo $RAND1" >> ~/.zsh_history 96 | echo "3 echo $RAND2" >> ~/.zsh_history 97 | echo "4 echo $RAND3" >> ~/.zsh_history 98 | echo "5 echo $RAND4" >> ~/.zsh_history 99 | zhps -y -r $UID -s $UID && zhpl -y 100 | check_history "^1 echo $RAND0$" 101 | check_history "^2 echo $RAND1$" 102 | check_history "^3 echo $RAND2$" 103 | check_history "^4 echo $RAND3$" 104 | check_history "^5 echo $RAND4$" 105 | success "SUCCESS" 106 | 107 | info "TEST SYNC HISTORY MULTI-LINE 0" 108 | setup 109 | echo "1 for i in {1..3}; do 110 | echo \$i 111 | done" >> ~/.zsh_history 112 | zhps -y -r $UID && zhpl -y 113 | check_history "^1 for i in \{1..3\}; do\necho \\\$i\ndone$" 114 | success "SUCCESS" 115 | 116 | info "TEST SYNC HISTORY MULTI-LINE PERL" 117 | setup 118 | alias sed="echo" 119 | echo "1 for i in {1..3}; do 120 | echo \$i 121 | done" >> ~/.zsh_history 122 | zhps -y -r $UID && zhpl -y 123 | check_history "^1 for i in \{1..3\}; do\necho \\\$i\ndone$" 124 | unalias sed 125 | success "SUCCESS" 126 | -------------------------------------------------------------------------------- /test/zshrc: -------------------------------------------------------------------------------- 1 | # If you come from bash you might have to change your $PATH. 2 | # export PATH=$HOME/bin:/usr/local/bin:$PATH 3 | 4 | # Path to your oh-my-zsh installation. 5 | export ZSH="/root/.oh-my-zsh" 6 | 7 | # Set name of the theme to load --- if set to "random", it will 8 | # load a random theme each time oh-my-zsh is loaded, in which case, 9 | # to know which specific one was loaded, run: echo $RANDOM_THEME 10 | # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes 11 | ZSH_THEME="robbyrussell" 12 | 13 | # Set list of themes to pick from when loading at random 14 | # Setting this variable when ZSH_THEME=random will cause zsh to load 15 | # a theme from this variable instead of looking in $ZSH/themes/ 16 | # If set to an empty array, this variable will have no effect. 17 | # ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) 18 | 19 | # Uncomment the following line to use case-sensitive completion. 20 | # CASE_SENSITIVE="true" 21 | 22 | # Uncomment the following line to use hyphen-insensitive completion. 23 | # Case-sensitive completion must be off. _ and - will be interchangeable. 24 | # HYPHEN_INSENSITIVE="true" 25 | 26 | # Uncomment one of the following lines to change the auto-update behavior 27 | # zstyle ':omz:update' mode disabled # disable automatic updates 28 | # zstyle ':omz:update' mode auto # update automatically without asking 29 | # zstyle ':omz:update' mode reminder # just remind me to update when it's time 30 | 31 | # Uncomment the following line to change how often to auto-update (in days). 32 | # zstyle ':omz:update' frequency 13 33 | 34 | # Uncomment the following line if pasting URLs and other text is messed up. 35 | # DISABLE_MAGIC_FUNCTIONS="true" 36 | 37 | # Uncomment the following line to disable colors in ls. 38 | # DISABLE_LS_COLORS="true" 39 | 40 | # Uncomment the following line to disable auto-setting terminal title. 41 | # DISABLE_AUTO_TITLE="true" 42 | 43 | # Uncomment the following line to enable command auto-correction. 44 | # ENABLE_CORRECTION="true" 45 | 46 | # Uncomment the following line to display red dots whilst waiting for completion. 47 | # You can also set it to another string to have that shown instead of the default red dots. 48 | # e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" 49 | # Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) 50 | # COMPLETION_WAITING_DOTS="true" 51 | 52 | # Uncomment the following line if you want to disable marking untracked files 53 | # under VCS as dirty. This makes repository status check for large repositories 54 | # much, much faster. 55 | # DISABLE_UNTRACKED_FILES_DIRTY="true" 56 | 57 | # Uncomment the following line if you want to change the command execution time 58 | # stamp shown in the history command output. 59 | # You can set one of the optional three formats: 60 | # "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" 61 | # or set a custom format using the strftime function format specifications, 62 | # see 'man strftime' for details. 63 | # HIST_STAMPS="mm/dd/yyyy" 64 | 65 | # Would you like to use another custom folder than $ZSH/custom? 66 | # ZSH_CUSTOM=/path/to/new-custom-folder 67 | 68 | # Which plugins would you like to load? 69 | # Standard plugins can be found in $ZSH/plugins/ 70 | # Custom plugins may be added to $ZSH_CUSTOM/plugins/ 71 | # Example format: plugins=(rails git textmate ruby lighthouse) 72 | # Add wisely, as too many plugins slow down shell startup. 73 | plugins=(git history-sync) 74 | 75 | source $ZSH/oh-my-zsh.sh 76 | 77 | # User configuration 78 | 79 | # export MANPATH="/usr/local/man:$MANPATH" 80 | 81 | # You may need to manually set your language environment 82 | # export LANG=en_US.UTF-8 83 | 84 | # Preferred editor for local and remote sessions 85 | # if [[ -n $SSH_CONNECTION ]]; then 86 | # export EDITOR='vim' 87 | # else 88 | # export EDITOR='mvim' 89 | # fi 90 | 91 | # Compilation flags 92 | # export ARCHFLAGS="-arch x86_64" 93 | 94 | # Set personal aliases, overriding those provided by oh-my-zsh libs, 95 | # plugins, and themes. Aliases can be placed here, though oh-my-zsh 96 | # users are encouraged to define aliases within the ZSH_CUSTOM folder. 97 | # For a full list of active aliases, run `alias`. 98 | # 99 | # Example aliases 100 | # alias zshconfig="mate ~/.zshrc" 101 | # alias ohmyzsh="mate ~/.oh-my-zsh" 102 | -------------------------------------------------------------------------------- /history-sync.plugin.zsh: -------------------------------------------------------------------------------- 1 | # ---------------------------------------------------------------- 2 | # Description 3 | # ---------------------------------------------------------------- 4 | # An Oh My Zsh plugin for GPG encrypted, Internet synchronized Zsh 5 | # history using Git. 6 | # 7 | # ---------------------------------------------------------------- 8 | # James Fraser - https://www.wulfgar.pro 9 | # ---------------------------------------------------------------- 10 | 11 | autoload -U colors && colors 12 | 13 | alias zhpl=history_sync_pull 14 | alias zhps=history_sync_push 15 | alias zhsync="history_sync_pull && history_sync_push" 16 | 17 | CP() { command cp "$@"; } 18 | MV() { command mv "$@"; } 19 | RM() { command rm "$@"; } 20 | TR() { LC_ALL=C command tr "$@"; } 21 | AWK() { command awk "$@"; } 22 | CAT() { command cat "$@"; } 23 | GIT() { command git "$@"; } 24 | GPG() { command gpg "$@"; } 25 | SED() { command sed "$@"; } 26 | DATE() { command date "$@"; } 27 | FOLD() { command fold "$@"; } 28 | GREP() { command grep "$@"; } 29 | HEAD() { command head "$@"; } 30 | PERL() { command perl "$@"; } 31 | SORT() { LC_ALL=C command sort "$@"; } 32 | MKTEMP() { command mktemp "$@"; } 33 | 34 | ZSH_HISTORY_PROJ="${ZSH_HISTORY_PROJ:-${HOME}/.zsh_history_proj}" 35 | ZSH_HISTORY_FILE_NAME="${ZSH_HISTORY_FILE_NAME:-.zsh_history}" 36 | ZSH_HISTORY_FILE="${ZSH_HISTORY_FILE:-${HOME}/${ZSH_HISTORY_FILE_NAME}}" 37 | ZSH_HISTORY_FILE_ENC_NAME="${ZSH_HISTORY_FILE_ENC_NAME:-zsh_history}" 38 | ZSH_HISTORY_FILE_ENC="${ZSH_HISTORY_FILE_ENC:-${ZSH_HISTORY_PROJ}/${ZSH_HISTORY_FILE_ENC_NAME}}" 39 | ZSH_HISTORY_FILE_DECRYPT_NAME="${ZSH_HISTORY_FILE_DECRYPT_NAME:-zsh_history_decrypted}" 40 | ZSH_HISTORY_FILE_MERGED_NAME="${ZSH_HISTORY_FILE_MERGED_NAME:-zsh_history_merged}" 41 | ZSH_HISTORY_COMMIT_MSG="${ZSH_HISTORY_COMMIT_MSG:-latest $(DATE)}" 42 | ZSH_HISTORY_DEFAULT_RECIPIENT="${ZSH_HISTORY_DEFAULT_RECIPIENT:-}" 43 | 44 | _print_git_error_msg() { 45 | echo "$bold_color${fg[red]}There's a problem with git repository: ${ZSH_HISTORY_PROJ}.$reset_color" 46 | return 47 | } 48 | 49 | _print_gpg_encrypt_error_msg() { 50 | echo "$bold_color${fg[red]}GPG failed to encrypt history file.$reset_color" 51 | return 52 | } 53 | 54 | _print_gpg_decrypt_error_msg() { 55 | echo "$bold_color${fg[red]}GPG failed to decrypt history file.$reset_color" 56 | return 57 | } 58 | 59 | _usage() { 60 | echo "Usage: [ [-r ...] [-y] ]" 1>&2 61 | echo 62 | echo "Optional args:" 63 | echo 64 | echo " -r recipients" 65 | echo " -s signers" 66 | echo " -y force" 67 | return 68 | } 69 | 70 | # "Squash" each multi-line command in the passed history files to one line 71 | _squash_multiline_commands_in_files() { 72 | # Create temporary files 73 | # Use global variables to use same path's in the restore-multi-line commands 74 | # function 75 | TMP_FILE_1=$(mktemp) 76 | TMP_FILE_2=$(mktemp) 77 | 78 | # Generate random character sequences to replace \n and anchor the first 79 | # line of a command (use global variable for new-line-replacement to use it 80 | # in the restore-multi-line commands function) 81 | NL_REPLACEMENT=$(TR -dc 'a-zA-Z0-9' < /dev/urandom | 82 | FOLD -w 32 | HEAD -n 1) 83 | local FIRST_LINE_ANCHOR=$(TR -dc 'a-zA-Z0-9' < /dev/urandom | 84 | FOLD -w 32 | HEAD -n 1) 85 | 86 | for i in "$ZSH_HISTORY_FILE" "$ZSH_HISTORY_FILE_DECRYPT_NAME"; do 87 | # Filter out multi-line commands and save them to a separate file 88 | GREP -v -B 1 '^: [0-9]\{1,10\}:[0-9]\+;' "${i}" | 89 | GREP -v -e '^--$' > "${TMP_FILE_1}" 90 | 91 | # Filter out multi-line commands and remove them from the original file 92 | GREP -v -x -F -f "${TMP_FILE_1}" "${i}" > "${TMP_FILE_2}" \ 93 | && MV "${TMP_FILE_2}" "${i}" 94 | 95 | # Add anchor before the first line of each command 96 | SED "s/\(^: [0-9]\{1,10\}:[0-9]\+;\)/${FIRST_LINE_ANCHOR} \1/" \ 97 | "${TMP_FILE_1}" > "${TMP_FILE_2}" \ 98 | && MV "${TMP_FILE_2}" "${TMP_FILE_1}" 99 | 100 | # Replace all \n with a sequence of symbols 101 | if [[ "$(SED --version 2>&1)" == *"GNU"* ]]; then 102 | SED ':a;N;$!ba;s/\n/'" ${NL_REPLACEMENT} "'/g' \ 103 | "${TMP_FILE_1}" > "${TMP_FILE_2}" 104 | else 105 | # Assume BSD `sed` 106 | PERL -0777 -pe 's/\n/'" ${NL_REPLACEMENT} "'/g' \ 107 | "${TMP_FILE_1}" > "${TMP_FILE_2}" 108 | fi 109 | MV "${TMP_FILE_2}" "${TMP_FILE_1}" 110 | 111 | # Replace first line anchor by \n 112 | SED "s/${FIRST_LINE_ANCHOR} \(: [0-9]\{1,10\}:[0-9]\+;\)/\n\1/g" \ 113 | "${TMP_FILE_1}" > "${TMP_FILE_2}" \ 114 | && MV "${TMP_FILE_2}" "${TMP_FILE_1}" 115 | 116 | # Merge squashed multiline commands to the history file 117 | CAT "${TMP_FILE_1}" >> "${i}" 118 | 119 | # Sort history file 120 | SORT -n < "${i}" > "${TMP_FILE_1}" && MV "${TMP_FILE_1}" "${i}" 121 | done 122 | } 123 | 124 | # Restore multi-line commands in the history file 125 | _restore_multiline_commands_in_file() { 126 | # Filter unnecessary lines from the history file (Binary file ... matches) 127 | # and save them in a separate file 128 | GREP -v '^: [0-9]\{1,10\}:[0-9]\+;' "$ZSH_HISTORY_FILE" > "${TMP_FILE_1}" 129 | 130 | # Filter out unnecessary lines and remove them from the original file 131 | GREP -v -x -F -f "${TMP_FILE_1}" "$ZSH_HISTORY_FILE" > "${TMP_FILE_2}" && \ 132 | MV "${TMP_FILE_2}" "$ZSH_HISTORY_FILE" 133 | 134 | # Replace the sequence of symbols by \n to restore multi-line commands 135 | SED "s/ ${NL_REPLACEMENT} /\n/g" "$ZSH_HISTORY_FILE" > "${TMP_FILE_1}" \ 136 | && MV "${TMP_FILE_1}" "$ZSH_HISTORY_FILE" 137 | 138 | # Unset global variables 139 | unset NL_REPLACEMENT TMP_FILE_1 TMP_FILE_2 140 | } 141 | 142 | # Pull current master, decrypt, and merge with .zsh_history 143 | history_sync_pull() { 144 | # Get options force 145 | local force=false 146 | while getopts y opt; do 147 | case "$opt" in 148 | y) 149 | force=true 150 | ;; 151 | esac 152 | done 153 | DIR=$(pwd) 154 | 155 | # Backup 156 | if [[ $force = false ]]; then 157 | CP -av "$ZSH_HISTORY_FILE" "$ZSH_HISTORY_FILE.backup" 1>&2 158 | fi 159 | 160 | 161 | # Clone if not exist 162 | if [[ ! -d "$ZSH_HISTORY_PROJ" ]]; then 163 | if [[ ! -v ZSH_HISTORY_GIT_REMOTE ]]; then 164 | _print_git_error_msg 165 | return 166 | fi 167 | 168 | "$GIT" clone "$ZSH_HISTORY_GIT_REMOTE" "$ZSH_HISTORY_PROJ" 169 | if [[ "$?" != 0 ]]; then 170 | _print_git_error_msg 171 | return 172 | fi 173 | fi 174 | 175 | # Pull 176 | cd "$ZSH_HISTORY_PROJ" && GIT pull 177 | if [[ "$?" != 0 ]]; then 178 | _print_git_error_msg 179 | cd "$DIR" 180 | return 181 | fi 182 | 183 | # Decrypt 184 | GPG --output "$ZSH_HISTORY_FILE_DECRYPT_NAME" --decrypt "$ZSH_HISTORY_FILE_ENC" 185 | if [[ "$?" != 0 ]]; then 186 | _print_gpg_decrypt_error_msg 187 | cd "$DIR" 188 | return 189 | fi 190 | 191 | # Check if EXTENDED_HISTORY is enabled, and if so, "squash" each multi-line 192 | # command in local and decrypted history files to one line 193 | [[ -o extendedhistory ]] && _squash_multiline_commands_in_files 194 | 195 | # Merge 196 | CAT "$ZSH_HISTORY_FILE" "$ZSH_HISTORY_FILE_DECRYPT_NAME" | \ 197 | AWK '/:[0-9]/ { if(s) { print s } s=$0 } !/:[0-9]/ { s=s"\n"$0 } END { print s }' | \ 198 | SORT -u > "$ZSH_HISTORY_FILE_MERGED_NAME" 199 | MV "$ZSH_HISTORY_FILE_MERGED_NAME" "$ZSH_HISTORY_FILE" 200 | RM "$ZSH_HISTORY_FILE_DECRYPT_NAME" 201 | cd "$DIR" 202 | 203 | # Check if EXTENDED_HISTORY is enabled, and if so, restore multi-line 204 | # commands in the local history file 205 | [[ -o extendedhistory ]] && _restore_multiline_commands_in_file 206 | # Strip trailing '\' if the next line is blank 207 | SED -E -i '/\\$/ { N; s/\\+\n$/\n/ }' "$ZSH_HISTORY_FILE" 208 | # Strip blank lines 209 | SED -i '/^$/d' "$ZSH_HISTORY_FILE" 210 | } 211 | 212 | # Encrypt and push current history to master 213 | history_sync_push() { 214 | # Get options recipients, force 215 | local recipients=() 216 | local signers=() 217 | local force=false 218 | while getopts r:s:y opt; do 219 | case "$opt" in 220 | r) 221 | recipients+=("$OPTARG") 222 | ;; 223 | s) 224 | signers+=("$OPTARG") 225 | ;; 226 | y) 227 | force=true 228 | ;; 229 | *) 230 | _usage 231 | return 232 | ;; 233 | esac 234 | done 235 | 236 | # Encrypt 237 | if ! [[ "${#recipients[@]}" > 0 ]]; then 238 | if [[ -n "$ZSH_HISTORY_DEFAULT_RECIPIENT" ]]; then 239 | recipients+=("$ZSH_HISTORY_DEFAULT_RECIPIENT") 240 | else 241 | echo -n "Please enter GPG recipient name: " 242 | read name 243 | recipients+=("$name") 244 | fi 245 | fi 246 | 247 | GPG_ENCRYPT_CMD_OPT="--yes -v " 248 | for r in "${recipients[@]}"; do 249 | GPG_ENCRYPT_CMD_OPT+="-r \"$r\" " 250 | done 251 | if [[ "${#signers[@]}" > 0 ]]; then 252 | GPG_ENCRYPT_CMD_OPT+="--sign " 253 | for s in "${signers[@]}"; do 254 | GPG_ENCRYPT_CMD_OPT+="--default-key \"$s\" " 255 | done 256 | fi 257 | 258 | if [[ "$GPG_ENCRYPT_CMD_OPT" != *"--sign"* ]]; then 259 | if [[ $force = false ]]; then 260 | echo -n "$bold_color${fg[yellow]}Do you want to sign with first key found in secret keyring (y/N)?$reset_color " 261 | read sign 262 | else 263 | sign='y' 264 | fi 265 | 266 | case "$sign" in 267 | [Yy]* ) 268 | GPG_ENCRYPT_CMD_OPT+="--sign " 269 | ;; 270 | * ) 271 | ;; 272 | esac 273 | fi 274 | 275 | if [[ "$GPG_ENCRYPT_CMD_OPT" =~ '.(-r).+.' ]]; then 276 | GPG_ENCRYPT_CMD_OPT+="--encrypt --armor --output \"$ZSH_HISTORY_FILE_ENC\" \"$ZSH_HISTORY_FILE\"" 277 | eval GPG "$GPG_ENCRYPT_CMD_OPT" 278 | if [[ "$?" != 0 ]]; then 279 | _print_gpg_encrypt_error_msg 280 | return 281 | fi 282 | 283 | # Commit 284 | if [[ $force = false ]]; then 285 | echo -n "$bold_color${fg[yellow]}Do you want to commit current local history file (y/N)?$reset_color " 286 | read commit 287 | else 288 | commit='y' 289 | fi 290 | 291 | if [[ -n "$commit" ]]; then 292 | case "$commit" in 293 | [Yy]* ) 294 | DIR=$(pwd) 295 | cd "$ZSH_HISTORY_PROJ" && GIT add * && GIT commit -m "$ZSH_HISTORY_COMMIT_MSG" 296 | local local_status=$? 297 | 298 | if [[ $force = false ]]; then 299 | echo -n "$bold_color${fg[yellow]}Do you want to push to remote (y/N)?$reset_color " 300 | read push 301 | else 302 | push='y' 303 | fi 304 | 305 | if [[ -n "$push" ]]; then 306 | case "$push" in 307 | [Yy]* ) 308 | GIT push 309 | local_status=$? 310 | ;; 311 | esac 312 | fi 313 | 314 | cd "$DIR" 315 | if [[ "$local_status" != 0 ]]; then 316 | _print_git_error_msg 317 | return 318 | fi 319 | ;; 320 | * ) 321 | ;; 322 | esac 323 | fi 324 | fi 325 | } 326 | --------------------------------------------------------------------------------