-
Notifications
You must be signed in to change notification settings - Fork 25
/
history-sync.plugin.zsh
301 lines (262 loc) · 9.6 KB
/
history-sync.plugin.zsh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# ----------------------------------------------------------------
# Description
# -----------
# An Oh My Zsh plugin for GPG encrypted, Internet synchronized Zsh
# history using Git.
#
# ----------------------------------------------------------------
# Authors
# -------
#
# * James Fraser <[email protected]>
# https://www.wulfgar.pro
# ----------------------------------------------------------------
autoload -U colors && colors
alias zhpl=history_sync_pull
alias zhps=history_sync_push
alias zhsync="history_sync_pull && history_sync_push"
GIT=$(which git)
GPG=$(which gpg)
SED_VERSION=$(sed --version 2>&1)
ZSH_HISTORY_PROJ="${ZSH_HISTORY_PROJ:-${HOME}/.zsh_history_proj}"
ZSH_HISTORY_FILE_NAME="${ZSH_HISTORY_FILE_NAME:-.zsh_history}"
ZSH_HISTORY_FILE="${ZSH_HISTORY_FILE:-${HOME}/${ZSH_HISTORY_FILE_NAME}}"
ZSH_HISTORY_FILE_ENC_NAME="${ZSH_HISTORY_FILE_ENC_NAME:-zsh_history}"
ZSH_HISTORY_FILE_ENC="${ZSH_HISTORY_FILE_ENC:-${ZSH_HISTORY_PROJ}/${ZSH_HISTORY_FILE_ENC_NAME}}"
ZSH_HISTORY_FILE_DECRYPT_NAME="${ZSH_HISTORY_FILE_DECRYPT_NAME:-zsh_history_decrypted}"
ZSH_HISTORY_FILE_MERGED_NAME="${ZSH_HISTORY_FILE_MERGED_NAME:-zsh_history_merged}"
ZSH_HISTORY_COMMIT_MSG="${ZSH_HISTORY_COMMIT_MSG:-latest $(date)}"
function SED() {LC_ALL=C sed "$@";}
function AWK() {LC_ALL=C awk "$@";}
function GREP() {LC_ALL=C grep "$@";}
function SORT() {LC_ALL=C sort "$@";}
function TR() {LC_ALL=C tr "$@";}
function _print_git_error_msg() {
echo "$bold_color${fg[red]}There's a problem with git repository: ${ZSH_HISTORY_PROJ}.$reset_color"
return
}
function _print_gpg_encrypt_error_msg() {
echo "$bold_color${fg[red]}GPG failed to encrypt history file.$reset_color"
return
}
function _print_gpg_decrypt_error_msg() {
echo "$bold_color${fg[red]}GPG failed to decrypt history file.$reset_color"
return
}
function _usage() {
echo "Usage: [ [-r <string> ...] [-y] ]" 1>&2
echo
echo "Optional args:"
echo
echo " -r receipients"
echo " -y force"
return
}
# "Squash" each multi-line command in the passed history files to one line
function _squash_multiline_commands_in_files() {
# Create temporary files
# Use global variables to use same path's in the restore-multi-line commands
# function
TMP_FILE_1=$(mktemp)
TMP_FILE_2=$(mktemp)
# Generate random character sequences to replace \n and anchor the first
# line of a command (use global variable for new-line-replacement to use it
# in the restore-multi-line commands function)
NL_REPLACEMENT=$(TR -dc 'a-zA-Z0-9' < /dev/urandom |
fold -w 32 | head -n 1)
local FIRST_LINE_ANCHOR=$(TR -dc 'a-zA-Z0-9' < /dev/urandom |
fold -w 32 | head -n 1)
for i in "$ZSH_HISTORY_FILE" "$ZSH_HISTORY_FILE_DECRYPT_NAME"; do
# Filter out multi-line commands and save them to a separate file
GREP -v -B 1 '^: [0-9]\{1,10\}:[0-9]\+;' "${i}" |
GREP -v -e '^--$' > "${TMP_FILE_1}"
# Filter out multi-line commands and remove them from the original file
GREP -v -x -F -f "${TMP_FILE_1}" "${i}" > "${TMP_FILE_2}" \
&& mv "${TMP_FILE_2}" "${i}"
# Add anchor before the first line of each command
SED "s/\(^: [0-9]\{1,10\}:[0-9]\+;\)/${FIRST_LINE_ANCHOR} \1/" \
"${TMP_FILE_1}" > "${TMP_FILE_2}" \
&& mv "${TMP_FILE_2}" "${TMP_FILE_1}"
# Replace all \n with a sequence of symbols
if [[ "$SED_VERSION" == *"GNU"* ]]; then
SED ':a;N;$!ba;s/\n/'" ${NL_REPLACEMENT} "'/g' \
"${TMP_FILE_1}" > "${TMP_FILE_2}"
else
# Assume BSD `sed`
perl -0777 -pe 's/\n/'" ${NL_REPLACEMENT} "'/g' \
"${TMP_FILE_1}" > "${TMP_FILE_2}"
fi
mv "${TMP_FILE_2}" "${TMP_FILE_1}"
# Replace first line anchor by \n
SED "s/${FIRST_LINE_ANCHOR} \(: [0-9]\{1,10\}:[0-9]\+;\)/\n\1/g" \
"${TMP_FILE_1}" > "${TMP_FILE_2}" \
&& mv "${TMP_FILE_2}" "${TMP_FILE_1}"
# Merge squashed multiline commands to the history file
cat "${TMP_FILE_1}" >> "${i}"
# Sort history file
SORT -n < "${i}" > "${TMP_FILE_1}" && mv "${TMP_FILE_1}" "${i}"
done
}
# Restore multi-line commands in the history file
function _restore_multiline_commands_in_file() {
# Filter unnecessary lines from the history file (Binary file ... matches)
# and save them in a separate file
GREP -v '^: [0-9]\{1,10\}:[0-9]\+;' "$ZSH_HISTORY_FILE" > "${TMP_FILE_1}"
# Filter out unnecessary lines and remove them from the original file
GREP -v -x -F -f "${TMP_FILE_1}" "$ZSH_HISTORY_FILE" > "${TMP_FILE_2}" && \
mv "${TMP_FILE_2}" "$ZSH_HISTORY_FILE"
# Replace the sequence of symbols by \n to restore multi-line commands
SED "s/ ${NL_REPLACEMENT} /\n/g" "$ZSH_HISTORY_FILE" > "${TMP_FILE_1}" \
&& mv "${TMP_FILE_1}" "$ZSH_HISTORY_FILE"
# Unset global variables
unset NL_REPLACEMENT TMP_FILE_1 TMP_FILE_2
}
# Pull current master, decrypt, and merge with .zsh_history
function history_sync_pull() {
# Get options force
local force=false
while getopts y opt; do
case "$opt" in
y)
force=true
;;
esac
done
DIR=$(pwd)
# Backup
if [[ $force = false ]]; then
cp -av "$ZSH_HISTORY_FILE" "$ZSH_HISTORY_FILE.backup" 1>&2
fi
# Pull
cd "$ZSH_HISTORY_PROJ" && "$GIT" pull
if [[ "$?" != 0 ]]; then
_print_git_error_msg
cd "$DIR"
return
fi
# Decrypt
"$GPG" --output "$ZSH_HISTORY_FILE_DECRYPT_NAME" --decrypt "$ZSH_HISTORY_FILE_ENC"
if [[ "$?" != 0 ]]; then
_print_gpg_decrypt_error_msg
cd "$DIR"
return
fi
# Check if EXTENDED_HISTORY is enabled, and if so, "squash" each multi-line
# command in local and decrypted history files to one line
[[ -o extendedhistory ]] && _squash_multiline_commands_in_files
# Merge
cat "$ZSH_HISTORY_FILE" "$ZSH_HISTORY_FILE_DECRYPT_NAME" | \
AWK '/:[0-9]/ { if(s) { print s } s=$0 } !/:[0-9]/ { s=s"\n"$0 } END { print s }' \
| SORT -u > "$ZSH_HISTORY_FILE_MERGED_NAME"
mv "$ZSH_HISTORY_FILE_MERGED_NAME" "$ZSH_HISTORY_FILE"
rm "$ZSH_HISTORY_FILE_DECRYPT_NAME"
cd "$DIR"
# Check if EXTENDED_HISTORY is enabled, and if so, restore multi-line
# commands in the local history file
[[ -o extendedhistory ]] && _restore_multiline_commands_in_file
SED -i '/^$/d' "$ZSH_HISTORY_FILE"
}
# Encrypt and push current history to master
function history_sync_push() {
# Get options recipients, force
local recipients=()
local signers=()
local force=false
while getopts r:s:y opt; do
case "$opt" in
r)
recipients+="$OPTARG"
;;
s)
signers+="$OPTARG"
;;
y)
force=true
;;
*)
_usage
return
;;
esac
done
# Encrypt
if ! [[ "${#recipients[@]}" > 0 ]]; then
echo -n "Please enter GPG recipient name: "
read name
recipients+="$name"
fi
ENCRYPT_CMD="$GPG --yes -v "
for r in "${recipients[@]}"; do
ENCRYPT_CMD+="-r \"$r\" "
done
if [[ "${#signers[@]}" > 0 ]]; then
ENCRYPT_CMD+="--sign "
for s in "${signers[@]}"; do
ENCRYPT_CMD+="--default-key \"$s\" "
done
fi
if [[ "$ENCRYPT_CMD" != *"--sign"* ]]; then
if [[ $force = false ]]; then
echo -n "$bold_color${fg[yellow]}Do you want to sign with first key found in secret keyring (y/N)?$reset_color "
read sign
else
sign='y'
fi
case "$sign" in
[Yy]* )
ENCRYPT_CMD+="--sign "
;;
* )
;;
esac
fi
if [[ "$ENCRYPT_CMD" =~ '.(-r).+.' ]]; then
ENCRYPT_CMD+="--encrypt --armor --output $ZSH_HISTORY_FILE_ENC $ZSH_HISTORY_FILE"
eval "$ENCRYPT_CMD"
if [[ "$?" != 0 ]]; then
_print_gpg_encrypt_error_msg
return
fi
# Commit
if [[ $force = false ]]; then
echo -n "$bold_color${fg[yellow]}Do you want to commit current local history file (y/N)?$reset_color "
read commit
else
commit='y'
fi
if [[ -n "$commit" ]]; then
case "$commit" in
[Yy]* )
DIR=$(pwd)
cd "$ZSH_HISTORY_PROJ" && "$GIT" add * && "$GIT" commit -m "$ZSH_HISTORY_COMMIT_MSG"
if [[ $force = false ]]; then
echo -n "$bold_color${fg[yellow]}Do you want to push to remote (y/N)?$reset_color "
read push
else
push='y'
fi
if [[ -n "$push" ]]; then
case "$push" in
[Yy]* )
"$GIT" push
if [[ "$?" != 0 ]]; then
_print_git_error_msg
cd "$DIR"
return
fi
cd "$DIR"
;;
esac
fi
if [[ "$?" != 0 ]]; then
_print_git_error_msg
cd "$DIR"
return
fi
;;
* )
;;
esac
fi
fi
}