from origin repo gh:ddworken/hishtory, commit 480630e9181167b51554f4407db55717d9b7e4dd

This commit is contained in:
setop
2022-11-13 10:56:08 +01:00
commit 96d048fba6
99 changed files with 12962 additions and 0 deletions

34
client/lib/config.fish Normal file
View File

@@ -0,0 +1,34 @@
function _hishtory_post_exec --on-event fish_postexec
# Runs after <ENTER>, but before the command is executed
set --global _hishtory_command $argv
set --global _hishtory_start_time (date +%s)
end
set --global _hishtory_first_prompt 1
function __hishtory_on_prompt --on-event fish_prompt
# Runs after the command is executed in order to render the prompt
# $? contains the exit code
set _hishtory_exit_code $status
if [ -n "$_hishtory_first_prompt" ]
set --global -e _hishtory_first_prompt
else if [ -n "$_hishtory_command" ]
hishtory saveHistoryEntry fish $_hishtory_exit_code "$_hishtory_command" $_hishtory_start_time & # Background Run
# hishtory saveHistoryEntry fish $_hishtory_exit_code "$_hishtory_command" $_hishtory_start_time # Foreground Run
set --global -e _hishtory_command # Unset _hishtory_command so we don't double-save entries when fish_prompt is invoked but fish_postexec isn't
end
end
function __hishtory_on_control_r
set -l tmp (mktemp -t fish.XXXXXX)
set -x init_query (commandline -b)
HISHTORY_TERM_INTEGRATION=1 hishtory tquery $init_query > $tmp
set -l res $status
commandline -f repaint
if [ -s $tmp ]
commandline -r (cat $tmp)
end
rm -f $tmp
end
[ (hishtory config-get enable-control-r) = true ] && bind \cr __hishtory_on_control_r

46
client/lib/config.sh Normal file
View File

@@ -0,0 +1,46 @@
# This script should be sourced inside of .bashrc to integrate bash with hishtory
# Include guard. This file is sourced in multiple places, but we want it to only execute once.
# This trick is from https://stackoverflow.com/questions/7518584/is-there-any-mechanism-in-shell-script-alike-include-guard-in-c
if [ -n "$__hishtory_bash_config_sourced" ]; then return; fi
__hishtory_bash_config_sourced=`date`
# Implementation of running before/after every command based on https://jichu4n.com/posts/debug-trap-and-prompt_command-in-bash/
function __hishtory_precommand() {
if [ -z "$HISHTORY_AT_PROMPT" ]; then
return
fi
unset HISHTORY_AT_PROMPT
# Run before every command
HISHTORY_START_TIME=`date +%s`
}
trap "__hishtory_precommand" DEBUG
HISHTORY_FIRST_PROMPT=1
function __hishtory_postcommand() {
EXIT_CODE=$?
HISHTORY_AT_PROMPT=1
if [ -n "$HISHTORY_FIRST_PROMPT" ]; then
unset HISHTORY_FIRST_PROMPT
return
fi
# Run after every prompt
(hishtory saveHistoryEntry bash $EXIT_CODE "`history 1`" $HISHTORY_START_TIME &) # Background Run
# hishtory saveHistoryEntry bash $EXIT_CODE "`history 1`" $HISHTORY_START_TIME # Foreground Run
}
PROMPT_COMMAND="__hishtory_postcommand; $PROMPT_COMMAND"
export HISTTIMEFORMAT=$HISTTIMEFORMAT
__history_control_r() {
READLINE_LINE=$(HISHTORY_TERM_INTEGRATION=1 hishtory tquery "$READLINE_LINE" | tr -d '\n')
READLINE_POINT=0x7FFFFFFF
}
__hishtory_bind_control_r() {
bind -x '"\C-r": __history_control_r'
}
[ "$(hishtory config-get enable-control-r)" = true ] && __hishtory_bind_control_r

37
client/lib/config.zsh Normal file
View File

@@ -0,0 +1,37 @@
autoload -U add-zsh-hook
add-zsh-hook zshaddhistory _hishtory_add
add-zsh-hook precmd _hishtory_precmd
_hishtory_first_prompt=1
function _hishtory_add() {
# Runs after <ENTER>, but before the command is executed
# $1 contains the command that was run
_hishtory_command=$1
_hishtory_start_time=`date +%s`
}
function _hishtory_precmd() {
# Runs after the command is executed in order to render the prompt
# $? contains the exit code
_hishtory_exit_code=$?
if [ -n "$_hishtory_first_prompt" ]; then
unset _hishtory_first_prompt
return
fi
(hishtory saveHistoryEntry zsh $_hishtory_exit_code "$_hishtory_command" $_hishtory_start_time &) # Background Run
# hishtory saveHistoryEntry zsh $_hishtory_exit_code "$_hishtory_command" $_hishtory_start_time # Foreground Run
}
_hishtory_widget() {
BUFFER=$(HISHTORY_TERM_INTEGRATION=1 hishtory tquery $BUFFER | tr -d '\n')
CURSOR=${#BUFFER}
zle reset-prompt
}
_hishtory_bind_control_r() {
zle -N _hishtory_widget
bindkey '^R' _hishtory_widget
}
[ "$(hishtory config-get enable-control-r)" = true ] && _hishtory_bind_control_r

View File

@@ -0,0 +1,6 @@
CWD Hostname Exit Code Command
/ ghaction-runner-hostname 0 hishtory config-set displayed-columns CWD Hostname 'Exit Code' Command
/ ghaction-runner-hostname 0 ls /tmp/ &
/ ghaction-runner-hostname 0 echo "foo"
/ ghaction-runner-hostname 0 echo bar
/ ghaction-runner-hostname 0 echo foo

View File

@@ -0,0 +1,3 @@
Hostname CWD Timestamp Runtime Exit Code Command
localhost ~/foo/ 2022/Apr/16 01:03 24s 3 table_cmd2
localhost /tmp/ 2022/Apr/16 01:03 4s 2 table_cmd1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

View File

@@ -0,0 +1,30 @@
-pipefail -tablesizing
Search Query: > -pipefail -tablesizing
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost ~/foo/ 2022/Apr/16 01:03 24s 3 table_cmd2 │
│ localhost /tmp/ 2022/Apr/16 01:03 4s 2 table_cmd1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,30 @@
-pipefail -tablesizing
Search Query: > -pipefail -tablesizing
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost ~/foo/ 2022/Apr/16 01:03 24s 3 table_cmd2 │
│ localhost /tmp/ 2022/Apr/16 01:03 4s 2 table_cmd1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 echo 'aaaaaa bbbb' │
│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,14 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 echo 'aaaaaa bbbb' │
│ localhost /tmp/ Oct 17 2022 21:43:16 PDT 3s 2 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > cwd:/tmp/ ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 2 ls ~/bar/ │
│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 ls ~/foo/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,2 @@
bash-5.2$ source /Users/david/.bashrc
bash-5.2$ echo

View File

@@ -0,0 +1,3 @@
Welcome to fish, the friendly interactive shell
Type help for instructions on how to use fish
david@Davids-MacBook-Air ~/c/hishtory (master)> echo 'aaaaaa bbbb'

View File

@@ -0,0 +1 @@
david@Davids-MacBook-Air hishtory % echo

View File

@@ -0,0 +1,26 @@
Search Query: > ls
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:36 PDT 3s 2 echo 'bar' & │
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 echo 'aaaaaa bbbb' │
│ localhost /tmp/ Oct 17 2022 21:43:26 PDT 3s 2 ls ~/bar/ │
│ localhost /tmp/ Oct 17 2022 21:43:21 PDT 3s 2 ls ~/foo/ │
│ server /etc/ Oct 17 2022 21:43:16 PDT 3s 127 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > ls
┌─────────────────────────────────────────────────────────────────────────────┐
│ Hostname Exit Code Command foo │
│─────────────────────────────────────────────────────────────────────────────│
│ ghaction-runner-hostname 0 ls / foo │
│ localhost 2 ls ~/bar/ │
│ localhost 2 ls ~/foo/ │
│ server 127 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > echo
┌─────────────────────────────────────────────────────────────────────────────┐
│ Hostname Exit Code Command foo │
│─────────────────────────────────────────────────────────────────────────────│
│ localhost 2 echo 'bar' & │
│ localhost 2 echo 'aaaaaa bbbb' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > asdf
┌─────────────────────────────────────────────────────────────────────────────┐
│ Hostname Exit Code Command foo │
│─────────────────────────────────────────────────────────────────────────────│
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > echo
┌─────────────────────────────────────────────────────────────────────────────┐
│ Hostname Exit Code Command foo │
│─────────────────────────────────────────────────────────────────────────────│
│ localhost 2 echo 'bar' & │
│ localhost 2 echo 'aaaaaa bbbb' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > echo
┌────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Hostname CWD Timestamp Runtime Exit Code Command │
│────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ localhost /tmp/ Oct 17 2022 21:43:36 PDT 3s 2 echo 'bar' & │
│ localhost /tmp/ Oct 17 2022 21:43:31 PDT 3s 2 echo 'aaaaaa bbbb' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,2 @@
bash-5.2$ source /Users/david/.bashrc
(reverse-i-search)`':

View File

@@ -0,0 +1,26 @@
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────┐
│ Hostname Exit Code Command foo │
│─────────────────────────────────────────────────────────────────────────────│
│ ghaction-runner-hostname 0 ls / foo │
│ localhost 2 echo 'bar' & │
│ localhost 2 echo 'aaaaaa bbbb' │
│ localhost 2 ls ~/bar/ │
│ localhost 2 ls ~/foo/ │
│ server 127 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,26 @@
Search Query: > ls
┌────────────────────────────────────────────────────┐
│ Hostname Exit Code Command │
│────────────────────────────────────────────────────│
│ localhost 2 echo 'bar' & │
│ localhost 2 echo 'aaaaaa bbbb' │
│ localhost 2 ls ~/bar/ │
│ localhost 2 ls ~/foo/ │
│ server 127 ls ~/ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,3 @@
Welcome to fish, the friendly interactive shell
Type help for instructions on how to use fish
david@Davids-MacBook-Air ~/c/hishtory (master)>

View File

@@ -0,0 +1,2 @@
david@Davids-MacBook-Air hishtory %
bck-i-search: _

View File

@@ -0,0 +1,4 @@
export FOOBAR='hello'
echo $FOOBAR world
cd /
echo baz

View File

@@ -0,0 +1,10 @@
Exit Code git_remote Command
0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command
0 echo bar
0 cd /
0 git@github.com:ddworken/hishtory.git echo foo
0 git@github.com:ddworken/hishtory.git hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true'
0 echo baz
0 cd /
0 echo $FOOBAR world
0 export FOOBAR='hello'

View File

@@ -0,0 +1,10 @@
Exit Code git_remote Command
0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command
0 echo bar
0 cd /
0 https://github.com/ddworken/hishtory echo foo
0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || true'
0 echo baz
0 cd /
0 echo $FOOBAR world
0 export FOOBAR='hello'

View File

@@ -0,0 +1,31 @@
bash-5.2$ source /Users/david/.bashrc
bash-5.2$ hishtory tquery -pipefail
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │
│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │
│ 0 cd / │
│ 0 git@github.com:ddworken/hishtory.git echo foo │
│ 0 git@github.com:ddworken/hishtory.git hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │
│ 0 echo baz │
│ 0 cd / │
│ 0 echo $FOOBAR world │
│ 0 export FOOBAR='hello' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,31 @@
bash-5.2$ source /Users/runner/.bashrc
bash-5.2$ hishtory tquery -pipefail
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │
│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │
│ 0 cd / │
│ 0 https://github.com/ddworken/hishtory echo foo │
│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │
│ 0 echo baz │
│ 0 cd / │
│ 0 echo $FOOBAR world │
│ 0 export FOOBAR='hello' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,31 @@
runner@ghaction-runner-hostname:~/work/hishtory/hishtory$ source /home/runner/.bashrc
runner@ghaction-runner-hostname:~/work/hishtory/hishtory$ hishtory tquery -pipefail
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │
│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │
│ 0 cd / │
│ 0 https://github.com/ddworken/hishtory echo foo │
│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │
│ 0 echo baz │
│ 0 cd / │
│ 0 echo $FOOBAR world │
│ 0 export FOOBAR='hello' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,30 @@
david@Davids-MacBook-Air hishtory % hishtory tquery -pipefail
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │
│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 git@github.com:ddworken/hishtory.git hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │
│ 0 cd / │
│ 0 git@github.com:ddworken/hishtory.git echo foo │
│ 0 git@github.com:ddworken/hishtory.git hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │
│ 0 echo baz │
│ 0 cd / │
│ 0 echo $FOOBAR world │
│ 0 export FOOBAR='hello' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,30 @@
runner@ghaction-runner-hostname hishtory % hishtory tquery -pipefail
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │
│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │
│ 0 cd / │
│ 0 https://github.com/ddworken/hishtory echo foo │
│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │
│ 0 echo baz │
│ 0 cd / │
│ 0 echo $FOOBAR world │
│ 0 export FOOBAR='hello' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,30 @@
ghaction-runner-hostname% hishtory tquery -pipefail
Search Query: > -pipefail
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Exit Code git_remote Command │
│─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ 0 https://github.com/ddworken/hishtory hishtory config-set displayed-columns 'Exit Code' git_remote Command │
│ 0 echo bar │
│ 0 cd / │
│ 0 https://github.com/ddworken/hishtory echo foo │
│ 0 https://github.com/ddworken/hishtory hishtory config-add custom-column git_remote '(git remote -v 2>/dev/null | grep origin 1>/dev/null ) && git remote get-url origin || … │
│ 0 echo baz │
│ 0 cd / │
│ 0 echo $FOOBAR world │
│ 0 export FOOBAR='hello' │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,3 @@
Hostname Command
localhost table_cmd2
localhost table_cmd1

View File

@@ -0,0 +1,3 @@
Hostname Exit Code Command
localhost 3 table_cmd2
localhost 2 table_cmd1

View File

@@ -0,0 +1,3 @@
Hostname Exit Code Command CWD
localhost 3 table_cmd2 ~/foo/
localhost 2 table_cmd1 /tmp/

View File

@@ -0,0 +1,7 @@
Hostname Exit Code Command CWD
localhost 2 while : /tmp/
do
ls /table/
done
localhost 3 table_cmd2 ~/foo/
localhost 2 table_cmd1 /tmp/

View File

@@ -0,0 +1,9 @@
Hostname Exit Code Command CWD foo
ghaction-runner-hostname 0 echo table-2 / aaaaaaaaaaaaa
ghaction-runner-hostname 0 echo table-1 / aaaaaaaaaaaaa
localhost 2 while : /tmp/
do
ls /table/
done
localhost 3 table_cmd2 ~/foo/
localhost 2 table_cmd1 /tmp/

View File

@@ -0,0 +1,3 @@
Hostname CWD Timestamp Runtime Exit Code Command
localhost ~/foo/ Apr 16 2022 01:03:16 PDT 24s 3 table_cmd2
localhost /tmp/ Apr 16 2022 01:03:06 PDT 4s 2 table_cmd1

View File

@@ -0,0 +1,38 @@
set -emo pipefail
hishtory status
set -emo pipefail
hishtory query
ls /a
ls /bar
ls /foo
echo foo
echo bar
hishtory enable
echo thisisrecorded
set -emo pipefail
hishtory query
set -emo pipefail
hishtory query foo
echo hello | grep complex | sed s/h/i/g; echo baz && echo "fo 'o" # mycommand
set -emo pipefail
hishtory query complex
set -emo pipefail
hishtory query
set -emo pipefail
echo mynewcommand
set -emo pipefail
hishtory query
set -emo pipefail
hishtory query
set -emo pipefail
echo mynewercommand
set -emo pipefail
hishtory query
othercomputer
set -emo pipefail
hishtory query
set -emo pipefail
hishtory reupload
set -emo pipefail
hishtory export | grep -v pipefail | grep -v '/tmp/client install'
set -emo pipefail

View File

@@ -0,0 +1,24 @@
Hostname Exit Code Command
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory config-set displayed-columns Hostname 'Exit Code' Command
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory export | grep -v pipefail | grep -v '/tmp/client install'
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory reupload
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -emo pipefail
localhost 2 othercomputer
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 echo mynewercommand
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 echo mynewcommand
ghaction-runner-hostname 0 set -emo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -emo pipefail

View File

@@ -0,0 +1,24 @@
Hostname Exit Code Command
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory config-set displayed-columns Hostname 'Exit Code' Command
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory export | grep -v pipefail | grep -v '/tmp/client install'
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory reupload
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -eo pipefail
localhost 2 othercomputer
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 echo mynewercommand
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 echo mynewcommand
ghaction-runner-hostname 0 set -eo pipefail
ghaction-runner-hostname 0 hishtory query
ghaction-runner-hostname 0 set -eo pipefail

View File

@@ -0,0 +1,38 @@
set -eo pipefail
hishtory status
set -eo pipefail
hishtory query
ls /a
ls /bar
ls /foo
echo foo
echo bar
hishtory enable
echo thisisrecorded
set -eo pipefail
hishtory query
set -eo pipefail
hishtory query foo
echo hello | grep complex | sed s/h/i/g; echo baz && echo "fo 'o" # mycommand
set -eo pipefail
hishtory query complex
set -eo pipefail
hishtory query
set -eo pipefail
echo mynewcommand
set -eo pipefail
hishtory query
set -eo pipefail
hishtory query
set -eo pipefail
echo mynewercommand
set -eo pipefail
hishtory query
othercomputer
set -eo pipefail
hishtory query
set -eo pipefail
hishtory reupload
set -eo pipefail
hishtory export | grep -v pipefail | grep -v '/tmp/client install'
set -eo pipefail

View File

@@ -0,0 +1,7 @@
echo foo
echo foo
echo baz
echo baz
echo foo
hishtory config-set displayed-columns 'Exit Code' Command
hishtory config-set filter-duplicate-commands true

View File

@@ -0,0 +1,6 @@
Exit Code Command
0 hishtory config-set filter-duplicate-commands true
0 hishtory config-set displayed-columns 'Exit Code' Command
0 echo foo
0 echo baz
0 echo foo

View File

@@ -0,0 +1,30 @@
-pipefail
Search Query: > -pipefail
┌───────────────────────────────────────────────────────────────────────────┐
│ Exit Code Command │
│───────────────────────────────────────────────────────────────────────────│
│ 0 hishtory config-set filter-duplicate-commands true │
│ 0 hishtory config-set displayed-columns 'Exit Code' Command │
│ 0 echo foo │
│ 0 echo baz │
│ 0 echo foo │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,5 @@
echo foo
echo foo
echo baz
echo baz
echo foo

View File

@@ -0,0 +1,7 @@
Exit Code Command
0 hishtory config-set displayed-columns 'Exit Code' Command
0 echo foo
0 echo baz
0 echo baz
0 echo foo
0 echo foo

View File

@@ -0,0 +1,30 @@
-pipefail
Search Query: > -pipefail
┌───────────────────────────────────────────────────────────────────────────┐
│ Exit Code Command │
│───────────────────────────────────────────────────────────────────────────│
│ 0 hishtory config-set displayed-columns 'Exit Code' Command │
│ 0 echo foo │
│ 0 echo baz │
│ 0 echo baz │
│ 0 echo foo │
│ 0 echo foo │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────┘

View File

@@ -0,0 +1,2 @@
foo
bar

View File

@@ -0,0 +1,8 @@
bash-5.2$ source /Users/david/.bashrc
bash-5.2$ echo foo
foo
bash-5.2$ hishtory
bash: hishtory: command not found
bash-5.2$ echo bar
bar
bash-5.2$

View File

@@ -0,0 +1,7 @@
david@Davids-MacBook-Air hishtory % echo foo
foo
david@Davids-MacBook-Air hishtory % hishtory
zsh: command not found: hishtory
david@Davids-MacBook-Air hishtory % echo bar
bar
david@Davids-MacBook-Air hishtory %

View File

@@ -0,0 +1,2 @@
echo foo
echo baz

View File

@@ -0,0 +1 @@
Are you sure you want to uninstall hiSHtory and delete all locally saved history data [y/N]Successfully uninstalled hishtory, please restart your terminal...

1700
client/lib/lib.go Normal file

File diff suppressed because it is too large Load Diff

435
client/lib/lib_test.go Normal file
View File

@@ -0,0 +1,435 @@
package lib
import (
"os"
"os/user"
"path"
"reflect"
"strings"
"testing"
"time"
"github.com/ddworken/hishtory/client/data"
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/shared/testutils"
)
func TestSetup(t *testing.T) {
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
homedir, err := os.UserHomeDir()
testutils.Check(t, err)
if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err == nil {
t.Fatalf("hishtory secret file already exists!")
}
testutils.Check(t, Setup([]string{}))
if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err != nil {
t.Fatalf("hishtory secret file does not exist after Setup()!")
}
data, err := os.ReadFile(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH))
testutils.Check(t, err)
if len(data) < 10 {
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
}
config := hctx.GetConf(hctx.MakeContext())
if config.IsOffline != false {
t.Fatalf("hishtory config should have been offline")
}
}
func TestSetupOffline(t *testing.T) {
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
homedir, err := os.UserHomeDir()
testutils.Check(t, err)
if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err == nil {
t.Fatalf("hishtory secret file already exists!")
}
testutils.Check(t, Setup([]string{"", "", "--offline"}))
if _, err := os.Stat(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH)); err != nil {
t.Fatalf("hishtory secret file does not exist after Setup()!")
}
data, err := os.ReadFile(path.Join(homedir, data.HISHTORY_PATH, data.CONFIG_PATH))
testutils.Check(t, err)
if len(data) < 10 {
t.Fatalf("hishtory secret has unexpected length: %d", len(data))
}
config := hctx.GetConf(hctx.MakeContext())
if config.IsOffline != true {
t.Fatalf("hishtory config should have been offline, actual=%#v", string(data))
}
}
func TestBuildHistoryEntry(t *testing.T) {
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
testutils.Check(t, Setup([]string{}))
// Test building an actual entry for bash
entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", " 123 ls /foo ", "1641774958"})
testutils.Check(t, err)
if entry.ExitCode != 120 {
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
}
user, err := user.Current()
if err != nil {
t.Fatalf("failed to retrieve user: %v", err)
}
if entry.LocalUsername != user.Username {
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
}
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
}
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command)
}
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
}
if entry.StartTime.Unix() != 1641774958 {
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
}
// Test building an entry for zsh
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", "ls /foo\n", "1641774958"})
testutils.Check(t, err)
if entry.ExitCode != 120 {
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
}
if entry.LocalUsername != user.Username {
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
}
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
}
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command)
}
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
}
if entry.StartTime.Unix() != 1641774958 {
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
}
// Test building an entry for fish
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "fish", "120", "ls /foo\n", "1641774958"})
testutils.Check(t, err)
if entry.ExitCode != 120 {
t.Fatalf("history entry has unexpected exit code: %v", entry.ExitCode)
}
if entry.LocalUsername != user.Username {
t.Fatalf("history entry has unexpected user name: %v", entry.LocalUsername)
}
if !strings.HasPrefix(entry.CurrentWorkingDirectory, "/") && !strings.HasPrefix(entry.CurrentWorkingDirectory, "~/") {
t.Fatalf("history entry has unexpected cwd: %v", entry.CurrentWorkingDirectory)
}
if !strings.HasPrefix(entry.HomeDirectory, "/") {
t.Fatalf("history entry has unexpected home directory: %v", entry.HomeDirectory)
}
if entry.Command != "ls /foo" {
t.Fatalf("history entry has unexpected command: %v", entry.Command)
}
if !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-09T") && !strings.HasPrefix(entry.StartTime.Format(time.RFC3339), "2022-01-10T") {
t.Fatalf("history entry has incorrect date in the start time: %v", entry.StartTime.Format(time.RFC3339))
}
if entry.StartTime.Unix() != 1641774958 {
t.Fatalf("history entry has incorrect Unix time in the start time: %v", entry.StartTime.Unix())
}
// Test building an entry that is empty, and thus not saved
entry, err = BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "zsh", "120", " \n", "1641774958"})
testutils.Check(t, err)
if entry != nil {
t.Fatalf("expected history entry to be nil")
}
}
func TestBuildHistoryEntryWithTimestampStripping(t *testing.T) {
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
defer testutils.BackupAndRestore(t)()
defer testutils.RunTestServer()()
testutils.Check(t, Setup([]string{}))
testcases := []struct {
input, histtimeformat, expectedCommand string
}{
{" 123 ls /foo ", "", "ls /foo"},
{" 2389 [2022-09-28 04:38:32 +0000] echo", "", "[2022-09-28 04:38:32 +0000] echo"},
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[%F %T %z] ", "echo"},
}
for _, tc := range testcases {
conf := hctx.GetConf(hctx.MakeContext())
conf.LastSavedHistoryLine = ""
testutils.Check(t, hctx.SetConfig(conf))
os.Setenv("HISTTIMEFORMAT", tc.histtimeformat)
entry, err := BuildHistoryEntry(hctx.MakeContext(), []string{"unused", "saveHistoryEntry", "bash", "120", tc.input, "1641774958"})
testutils.Check(t, err)
if entry == nil {
t.Fatalf("entry is unexpectedly nil")
}
if entry.Command != tc.expectedCommand {
t.Fatalf("BuildHistoryEntry(%#v) returned %#v (expected=%#v)", tc.input, entry.Command, tc.expectedCommand)
}
}
}
func TestPersist(t *testing.T) {
defer testutils.BackupAndRestore(t)()
testutils.Check(t, hctx.InitConfig())
db := hctx.GetDb(hctx.MakeContext())
entry := testutils.MakeFakeHistoryEntry("ls ~/")
db.Create(entry)
var historyEntries []*data.HistoryEntry
result := db.Find(&historyEntries)
testutils.Check(t, result.Error)
if len(historyEntries) != 1 {
t.Fatalf("DB has %d entries, expected 1!", len(historyEntries))
}
dbEntry := historyEntries[0]
if !data.EntryEquals(entry, *dbEntry) {
t.Fatalf("DB data is different than input! \ndb =%#v \ninput=%#v", *dbEntry, entry)
}
}
func TestSearch(t *testing.T) {
defer testutils.BackupAndRestore(t)()
testutils.Check(t, hctx.InitConfig())
ctx := hctx.MakeContext()
db := hctx.GetDb(ctx)
// Insert data
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
db.Create(entry1)
entry2 := testutils.MakeFakeHistoryEntry("ls /bar")
db.Create(entry2)
// Search for data
results, err := Search(ctx, db, "ls", 5)
testutils.Check(t, err)
if len(results) != 2 {
t.Fatalf("Search() returned %d results, expected 2!", len(results))
}
if !data.EntryEquals(*results[0], entry2) {
t.Fatalf("Search()[0]=%#v, expected: %#v", results[0], entry2)
}
if !data.EntryEquals(*results[1], entry1) {
t.Fatalf("Search()[0]=%#v, expected: %#v", results[1], entry1)
}
}
func TestAddToDbIfNew(t *testing.T) {
// Set up
defer testutils.BackupAndRestore(t)()
testutils.Check(t, hctx.InitConfig())
db := hctx.GetDb(hctx.MakeContext())
// Add duplicate entries
entry1 := testutils.MakeFakeHistoryEntry("ls /foo")
AddToDbIfNew(db, entry1)
AddToDbIfNew(db, entry1)
entry2 := testutils.MakeFakeHistoryEntry("ls /foo")
AddToDbIfNew(db, entry2)
AddToDbIfNew(db, entry2)
AddToDbIfNew(db, entry1)
// Check there should only be two entries
var entries []data.HistoryEntry
result := db.Find(&entries)
if result.Error != nil {
t.Fatal(result.Error)
}
if len(entries) != 2 {
t.Fatalf("entries has an incorrect length: %d", len(entries))
}
}
func TestParseCrossPlatformInt(t *testing.T) {
res, err := parseCrossPlatformInt("123")
testutils.Check(t, err)
if res != 123 {
t.Fatalf("failed to parse cross platform int %d", res)
}
res, err = parseCrossPlatformInt("123N")
testutils.Check(t, err)
if res != 123 {
t.Fatalf("failed to parse cross platform int %d", res)
}
}
func TestBuildRegexFromTimeFormat(t *testing.T) {
testcases := []struct {
formatString, regex string
}{
{"%Y ", "[0-9]{4} "},
{"%F %T ", "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} "},
{"%F%T", "[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
{"%%", "%"},
{"%%%%", "%%"},
{"%%%Y", "%[0-9]{4}"},
{"%%%F%T", "%[0-9]{4}-[0-9]{2}-[0-9]{2}[0-9]{2}:[0-9]{2}:[0-9]{2}"},
}
for _, tc := range testcases {
if regex := buildRegexFromTimeFormat(tc.formatString); regex != tc.regex {
t.Fatalf("building a regex for %#v returned %#v (expected=%#v)", tc.formatString, regex, tc.regex)
}
}
}
func TestGetLastCommand(t *testing.T) {
testcases := []struct {
input, expectedOutput string
}{
{" 0 ls", "ls"},
{" 33 ls", "ls"},
{" 33 ls --aaaa foo bar ", "ls --aaaa foo bar"},
{" 2389 [2022-09-28 04:38:32 +0000] echo", "[2022-09-28 04:38:32 +0000] echo"},
}
for _, tc := range testcases {
actualOutput, err := getLastCommand(tc.input)
testutils.Check(t, err)
if actualOutput != tc.expectedOutput {
t.Fatalf("getLastCommand(%#v) returned %#v (expected=%#v)", tc.input, actualOutput, tc.expectedOutput)
}
}
}
func TestMaybeSkipBashHistTimePrefix(t *testing.T) {
defer testutils.BackupAndRestoreEnv("HISTTIMEFORMAT")()
testcases := []struct {
env, cmdLine, expected string
}{
{"%F %T ", "2019-07-12 13:02:31 sudo apt update", "sudo apt update"},
{"%F %T ", "2019-07-12 13:02:31 ls a b", "ls a b"},
{"%F %T ", "2019-07-12 13:02:31 ls a ", "ls a "},
{"%F %T ", "2019-07-12 13:02:31 ls a", "ls a"},
{"%F %T ", "2019-07-12 13:02:31 ls", "ls"},
{"%F %T ", "2019-07-12 13:02:31 ls -Slah", "ls -Slah"},
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
{"%F ", "2019-07-12 ls -Slah", "ls -Slah"},
{"%Y", "2019ls -Slah", "ls -Slah"},
{"%Y%Y", "20192020ls -Slah", "ls -Slah"},
{"%Y%Y", "20192020ls -Slah20192020", "ls -Slah20192020"},
{"", "ls -Slah", "ls -Slah"},
{"[%F %T] ", "[2019-07-12 13:02:31] sudo apt update", "sudo apt update"},
{"[%F a %T] ", "[2019-07-12 a 13:02:31] sudo apt update", "sudo apt update"},
{"aaa ", "aaa sudo apt update", "sudo apt update"},
{"%c ", "Sun Aug 19 02:56:02 2012 sudo apt update", "sudo apt update"},
{"%c ", "Sun Aug 19 02:56:02 2012 ls", "ls"},
{"[%c] ", "[Sun Aug 19 02:56:02 2012] ls", "ls"},
{"[%c %t] ", "[Sun Aug 19 02:56:02 2012 ] ls", "ls"},
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]ls", "ls"},
{"[%c %t]", "[Sun Aug 19 02:56:02 2012 ]foo", "foo"},
{"[%c %t", "[Sun Aug 19 02:56:02 2012 foo", "foo"},
{"[%F %T %z]", "[2022-09-28 04:17:06 +0000]foo", "foo"},
{"[%F %T %z] ", "[2022-09-28 04:17:06 +0000] foo", "foo"},
}
for _, tc := range testcases {
os.Setenv("HISTTIMEFORMAT", tc.env)
stripped, err := maybeSkipBashHistTimePrefix(tc.cmdLine)
testutils.Check(t, err)
if stripped != tc.expected {
t.Fatalf("skipping the time prefix returned %#v (expected=%#v for %#v)", stripped, tc.expected, tc.cmdLine)
}
}
}
func TestChunks(t *testing.T) {
testcases := []struct {
input []int
chunkSize int
output [][]int
}{
{[]int{1, 2, 3, 4, 5}, 2, [][]int{{1, 2}, {3, 4}, {5}}},
{[]int{1, 2, 3, 4, 5}, 3, [][]int{{1, 2, 3}, {4, 5}}},
{[]int{1, 2, 3, 4, 5}, 1, [][]int{{1}, {2}, {3}, {4}, {5}}},
{[]int{1, 2, 3, 4, 5}, 4, [][]int{{1, 2, 3, 4}, {5}}},
}
for _, tc := range testcases {
actual := chunks(tc.input, tc.chunkSize)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatal("chunks failure")
}
}
}
func TestZshWeirdness(t *testing.T) {
testcases := []struct {
input string
output string
}{
{": 1666062975:0;bash", "bash"},
{": 16660:0;ls", "ls"},
{"ls", "ls"},
{"0", "0"},
{"hgffddxsdsrzsz xddfgdxfdv gdfc ghcvhgfcfg vgv", "hgffddxsdsrzsz xddfgdxfdv gdfc ghcvhgfcfg vgv"},
}
for _, tc := range testcases {
actual := stripZshWeirdness(tc.input)
if !reflect.DeepEqual(actual, tc.output) {
t.Fatalf("weirdness failure for %#v", tc.input)
}
}
}
func TestParseTimeGenerously(t *testing.T) {
ts, err := parseTimeGenerously("2006-01-02T15:04:00-08:00")
testutils.Check(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02 T15:04:00 -08:00")
testutils.Check(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_T15:04:00_-08:00")
testutils.Check(t, err)
if ts.Unix() != 1136243040 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02T15:04:00")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_T15:04:00")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_15:04:00")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02T15:04")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02_15:04")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 15 || ts.Minute() != 4 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
ts, err = parseTimeGenerously("2006-01-02")
testutils.Check(t, err)
if ts.Year() != 2006 || ts.Month() != time.January || ts.Day() != 2 || ts.Hour() != 0 || ts.Minute() != 0 || ts.Second() != 0 {
t.Fatalf("parsed time incorrectly: %d", ts.Unix())
}
}

99
client/lib/slsa.go Normal file
View File

@@ -0,0 +1,99 @@
package lib
import (
"bufio"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/slsa-framework/slsa-verifier/options"
"github.com/slsa-framework/slsa-verifier/verifiers"
)
func verify(ctx *context.Context, provenance []byte, artifactHash, source, branch, versionTag string) error {
provenanceOpts := &options.ProvenanceOpts{
ExpectedSourceURI: source,
ExpectedBranch: &branch,
ExpectedDigest: artifactHash,
ExpectedVersionedTag: &versionTag,
}
builderOpts := &options.BuilderOpts{}
_, _, err := verifiers.Verify(*ctx, provenance, artifactHash, provenanceOpts, builderOpts)
return err
}
func checkForDowngrade(currentVersionS, newVersionS string) error {
currentVersion, err := strconv.Atoi(strings.TrimPrefix(currentVersionS, "v0."))
if err != nil {
return fmt.Errorf("failed to parse current version %#v", currentVersionS)
}
newVersion, err := strconv.Atoi(strings.TrimPrefix(newVersionS, "v0."))
if err != nil {
return fmt.Errorf("failed to parse updated version %#v", newVersionS)
}
if currentVersion > newVersion {
return fmt.Errorf("failed to update because the new version (%#v) is a downgrade compared to the current version (%#v)", newVersionS, currentVersionS)
}
return nil
}
func verifyBinary(ctx *context.Context, binaryPath, attestationPath, versionTag string) error {
if os.Getenv("HISHTORY_DISABLE_SLSA_ATTESTATION") == "true" {
return nil
}
resp, err := ApiGet("/api/v1/slsa-status?newVersion=" + versionTag)
if err != nil {
return nil
}
if string(resp) != "OK" {
fmt.Printf("SLSA verification is currently broken (%s), skipping SLSA validation...\n", string(resp))
return nil
}
if err := checkForDowngrade(Version, versionTag); err != nil && os.Getenv("HISHTORY_ALLOW_DOWNGRADE") == "true" {
return err
}
attestation, err := os.ReadFile(attestationPath)
if err != nil {
return fmt.Errorf("failed to read attestation file: %v", err)
}
hash, err := getFileHash(binaryPath)
if err != nil {
return err
}
return verify(ctx, attestation, hash, "github.com/ddworken/hishtory", "master", versionTag)
}
func getFileHash(binaryPath string) (string, error) {
binaryFile, err := os.Open(binaryPath)
if err != nil {
return "", fmt.Errorf("failed to read binary for verification purposes: %v", err)
}
defer binaryFile.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, binaryFile); err != nil {
return "", fmt.Errorf("failed to hash binary: %v", err)
}
hash := hex.EncodeToString(hasher.Sum(nil))
return hash, nil
}
func handleSlsaFailure(srcErr error) error {
fmt.Printf("\nFailed to verify SLSA provenance! This is likely due to a SLSA bug (SLSA is a brand new standard, and like all new things, has bugs). Ignoring this failure means falling back to the way most software does updates. Do you want to ignore this failure and update anyways? [y/N]")
reader := bufio.NewReader(os.Stdin)
resp, err := reader.ReadString('\n')
if err == nil && strings.TrimSpace(resp) == "y" {
fmt.Println("Proceeding with update...")
return nil
}
return fmt.Errorf("failed to verify SLSA provenance of the updated binary, aborting update (to bypass, set `export HISHTORY_DISABLE_SLSA_ATTESTATION=true`): %v", srcErr)
}

451
client/lib/tui.go Normal file
View File

@@ -0,0 +1,451 @@
package lib
import (
"context"
"fmt"
"os"
"strings"
_ "embed" // for embedding config.sh
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/ddworken/hishtory/client/hctx"
"github.com/muesli/termenv"
"golang.org/x/term"
)
const TABLE_HEIGHT = 20
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5
var selectedRow string = ""
var baseStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240"))
type errMsg error
type model struct {
// context
ctx *context.Context
// Model for the loading spinner.
spinner spinner.Model
// Whether data is still loading and the spinner should still be displayed.
isLoading bool
// Whether the TUI is quitting.
quitting bool
// The table used for displaying search results.
table table.Model
// The number of entries in the table.
numEntries int
// Whether the user has hit enter to select an entry and the TUI is thus about to quit.
selected bool
// The search box for the query
queryInput textinput.Model
// The query to run. Reset to nil after it was run.
runQuery *string
// The previous query that was run.
lastQuery string
// Unrecoverable error.
err error
// An error while searching. Recoverable and displayed as a warning message.
searchErr error
// Whether the device is offline. If so, a warning will be displayed.
isOffline bool
// A banner from the backend to be displayed. Generally an empty string.
banner string
}
type doneDownloadingMsg struct{}
type offlineMsg struct{}
type bannerMsg struct {
banner string
}
func initialModel(ctx *context.Context, t table.Model, initialQuery string, numEntries int) model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
queryInput := textinput.New()
queryInput.Placeholder = "ls"
queryInput.Focus()
queryInput.CharLimit = 156
queryInput.Width = 50
if initialQuery != "" {
queryInput.SetValue(initialQuery)
}
return model{ctx: ctx, spinner: s, isLoading: true, table: t, runQuery: &initialQuery, queryInput: queryInput, numEntries: numEntries}
}
func (m model) Init() tea.Cmd {
return m.spinner.Tick
}
func runQueryAndUpdateTable(m model, updateTable bool) model {
if (m.runQuery != nil && *m.runQuery != m.lastQuery) || updateTable {
if m.runQuery == nil {
m.runQuery = &m.lastQuery
}
rows, numEntries, err := getRows(m.ctx, hctx.GetConf(m.ctx).DisplayedColumns, *m.runQuery, PADDED_NUM_ENTRIES)
if err != nil {
m.searchErr = err
return m
} else {
m.searchErr = nil
}
m.numEntries = numEntries
if updateTable {
t, err := makeTable(m.ctx, rows)
if err != nil {
m.err = err
return m
}
m.table = t
}
m.table.SetRows(rows)
m.table.SetCursor(0)
m.lastQuery = *m.runQuery
m.runQuery = nil
}
if m.table.Cursor() >= m.numEntries {
// Ensure that we can't scroll past the end of the table
m.table.SetCursor(m.numEntries - 1)
}
return m
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "esc", "ctrl+c":
m.quitting = true
return m, tea.Quit
case "enter":
if m.numEntries != 0 {
m.selected = true
}
return m, tea.Quit
default:
t, cmd1 := m.table.Update(msg)
m.table = t
if strings.HasPrefix(msg.String(), "alt+") {
return m, tea.Batch(cmd1)
}
i, cmd2 := m.queryInput.Update(msg)
m.queryInput = i
searchQuery := m.queryInput.Value()
m.runQuery = &searchQuery
m = runQueryAndUpdateTable(m, false)
return m, tea.Batch(cmd1, cmd2)
}
case tea.WindowSizeMsg:
m = runQueryAndUpdateTable(m, true)
return m, nil
case errMsg:
m.err = msg
return m, nil
case offlineMsg:
m.isOffline = true
return m, nil
case bannerMsg:
m.banner = msg.banner
return m, nil
case doneDownloadingMsg:
m.isLoading = false
return m, nil
default:
var cmd tea.Cmd
if m.isLoading {
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
} else {
m.table, cmd = m.table.Update(msg)
return m, cmd
}
}
}
func (m model) View() string {
if m.err != nil {
return fmt.Sprintf("An unrecoverable error occured: %v\n", m.err)
}
if m.selected {
indexOfCommand := -1
for i, columnName := range hctx.GetConf(m.ctx).DisplayedColumns {
if columnName == "Command" {
indexOfCommand = i
break
}
}
if indexOfCommand == -1 {
selectedRow = "Error: Table doesn't have a column named `Command`?"
return ""
}
selectedRow = m.table.SelectedRow()[indexOfCommand]
return ""
}
if m.quitting {
return ""
}
loadingMessage := ""
if m.isLoading {
loadingMessage = fmt.Sprintf("%s Loading hishtory entries from other devices...", m.spinner.View())
}
warning := ""
if m.isOffline {
warning += "Warning: failed to contact the hishtory backend (are you offline?), so some results may be stale\n\n"
}
if m.searchErr != nil {
warning += fmt.Sprintf("Warning: failed to search: %v\n\n", m.searchErr)
}
return fmt.Sprintf("\n%s\n%s%s\nSearch Query: %s\n\n%s\n", loadingMessage, warning, m.banner, m.queryInput.View(), baseStyle.Render(m.table.View()))
}
func getRows(ctx *context.Context, columnNames []string, query string, numEntries int) ([]table.Row, int, error) {
db := hctx.GetDb(ctx)
config := hctx.GetConf(ctx)
data, err := Search(ctx, db, query, numEntries)
if err != nil {
return nil, 0, err
}
var rows []table.Row
lastCommand := ""
for i := 0; i < numEntries; i++ {
if i < len(data) {
entry := data[i]
if strings.TrimSpace(entry.Command) == strings.TrimSpace(lastCommand) && config.FilterDuplicateCommands {
continue
}
entry.Command = strings.ReplaceAll(entry.Command, "\n", " ") // TODO: handle multi-line commands better here
row, err := buildTableRow(ctx, columnNames, *entry)
if err != nil {
return nil, 0, fmt.Errorf("failed to build row for entry=%#v: %v", entry, err)
}
rows = append(rows, row)
lastCommand = entry.Command
} else {
rows = append(rows, table.Row{})
}
}
return rows, len(data), nil
}
func calculateColumnWidths(rows []table.Row) []int {
numColumns := len(rows[0])
neededColumnWidth := make([]int, numColumns)
for _, row := range rows {
for i, v := range row {
neededColumnWidth[i] = max(neededColumnWidth[i], len(v))
}
}
return neededColumnWidth
}
func getTerminalSize() (int, int, error) {
return term.GetSize(2)
}
var bigQueryResults []table.Row
func makeTableColumns(ctx *context.Context, columnNames []string, rows []table.Row) ([]table.Column, error) {
// Handle an initial query with no results
if len(rows) == 0 || len(rows[0]) == 0 {
allRows, _, err := getRows(ctx, columnNames, "", 25)
if err != nil {
return nil, err
}
return makeTableColumns(ctx, columnNames, allRows)
}
// Calculate the minimum amount of space that we need for each column for the current actual search
columnWidths := calculateColumnWidths(rows)
totalWidth := 20
for i, name := range columnNames {
columnWidths[i] = max(columnWidths[i], len(name))
totalWidth += columnWidths[i]
}
// Calculate the maximum column width that is useful for each column if we search for the empty string
if bigQueryResults == nil {
bigRows, _, err := getRows(ctx, columnNames, "", 1000)
if err != nil {
return nil, err
}
bigQueryResults = bigRows
}
maximumColumnWidths := calculateColumnWidths(bigQueryResults)
// Get the actual terminal width. If we're below this, opportunistically add some padding aiming for the maximum column widths
terminalWidth, _, err := getTerminalSize()
if err != nil {
return nil, fmt.Errorf("failed to get terminal size: %v", err)
}
for totalWidth < (terminalWidth - len(columnNames)) {
prevTotalWidth := totalWidth
for i := range columnNames {
if columnWidths[i] < maximumColumnWidths[i]+5 {
columnWidths[i] += 1
totalWidth += 1
}
}
if totalWidth == prevTotalWidth {
break
}
}
// And if we are too large from the initial query, let's shrink things to make the table fit. We'll use the heuristic of always shrinking the widest column.
for totalWidth > terminalWidth {
largestColumnIdx := -1
largestColumnSize := -1
for i := range columnNames {
if columnWidths[i] > largestColumnSize {
largestColumnIdx = i
largestColumnSize = columnWidths[i]
}
}
columnWidths[largestColumnIdx] -= 2
totalWidth -= 2
}
// And finally, create some actual columns!
columns := make([]table.Column, 0)
for i, name := range columnNames {
columns = append(columns, table.Column{Title: name, Width: columnWidths[i]})
}
return columns, nil
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func makeTable(ctx *context.Context, rows []table.Row) (table.Model, error) {
config := hctx.GetConf(ctx)
columns, err := makeTableColumns(ctx, config.DisplayedColumns, rows)
if err != nil {
return table.Model{}, err
}
km := table.KeyMap{
LineUp: key.NewBinding(
key.WithKeys("up", "alt+OA"),
key.WithHelp("↑", "scroll up"),
),
LineDown: key.NewBinding(
key.WithKeys("down", "alt+OB"),
key.WithHelp("↓", "scroll down"),
),
PageUp: key.NewBinding(
key.WithKeys("pgup"),
key.WithHelp("pgup", "page up"),
),
PageDown: key.NewBinding(
key.WithKeys("pgdown"),
key.WithHelp("pgdn", "page down"),
),
GotoTop: key.NewBinding(
key.WithKeys("home"),
key.WithHelp("home", "go to start"),
),
GotoBottom: key.NewBinding(
key.WithKeys("end"),
key.WithHelp("end", "go to end"),
),
}
_, terminalHeight, err := getTerminalSize()
if err != nil {
return table.Model{}, err
}
tableHeight := min(TABLE_HEIGHT, terminalHeight-12)
t := table.New(
table.WithColumns(columns),
table.WithRows(rows),
table.WithFocused(true),
table.WithHeight(tableHeight),
table.WithKeyMap(km),
)
s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderBottom(true).
Bold(false)
s.Selected = s.Selected.
Foreground(lipgloss.Color("229")).
Background(lipgloss.Color("57")).
Bold(false)
t.SetStyles(s)
t.Focus()
return t, nil
}
func TuiQuery(ctx *context.Context, gitCommit, initialQuery string) error {
lipgloss.SetColorProfile(termenv.ANSI)
rows, numEntries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, initialQuery, PADDED_NUM_ENTRIES)
if err != nil {
return err
}
t, err := makeTable(ctx, rows)
if err != nil {
return err
}
p := tea.NewProgram(initialModel(ctx, t, initialQuery, numEntries), tea.WithOutput(os.Stderr))
go func() {
err := RetrieveAdditionalEntriesFromRemote(ctx)
if err != nil {
p.Send(err)
}
p.Send(doneDownloadingMsg{})
}()
// Async: Process deletion requests
go func() {
err := ProcessDeletionRequests(ctx)
if err != nil {
p.Send(err)
}
}()
// Async: Check for any banner from the server
go func() {
banner, err := GetBanner(ctx, gitCommit)
if err != nil {
if IsOfflineError(err) {
p.Send(offlineMsg{})
} else {
p.Send(err)
}
}
p.Send(bannerMsg{banner: string(banner)})
}()
// Blocking: Start the TUI
err = p.Start()
if err != nil {
return err
}
if selectedRow == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" {
// Print out the initialQuery instead so that we don't clear the terminal
selectedRow = initialQuery
}
fmt.Printf("%s\n", selectedRow)
return nil
}