Skip to content

Instantly share code, notes, and snippets.

@kinggrowler
Last active February 7, 2020 17:24
Show Gist options
  • Save kinggrowler/fa4a543c823e89dfa7d9fef6d963abe4 to your computer and use it in GitHub Desktop.
Save kinggrowler/fa4a543c823e89dfa7d9fef6d963abe4 to your computer and use it in GitHub Desktop.
Bash functions to create and manage a command line journal using git commit messages as individual entries
# GIT JOURNAL
# v1.6
#
# These functions assume bash.
#
# Source this file to create a "journal" that uses git
# "commit messages" as individual, date-stamped journal entries.
# Original idea credited to Reddit user u/ceifeira in this thread about 'jrnl':
#
# https://reddit.com/r/programming/comments/2951ma/jrnl_the_command_line_journal/ciif8jl/
# There are a hundred better ways to create a command line journal (cf. http://jrnl.sh ).
# Use at your own risk! This is a PoC.
# Although the journal repository is created when sourced, it
# initially has zero entries. Your first entry can be
# created using the 'jec' function.
#
# You can also import a journal entry from text file using
# the 'jei' function; export entry using 'jex'.
# You can create multiple journals in the same
# repository by creating separate sections (branches).
# (See 'jbc' and related functions below.)
# Journal entries should follow the git standard:
# Subject - Blank line - Body
# Entries (before the most recent) CANNOT be edited!
# Older entries are IMMUTABLE!
# You can edit the MOST RECENT entry by using the 'jea' function.
# Also, "git_notes" can be used to attach a note to any previous
# entry if you wish to expand/clarify on what is written.
# NOTE: We explicitly set the git editor in git_config to use vim,
# including the lovely distraction-free Goyo plugin.
#
# https://github.com/junegunn/goyo.vim
#
# If Goyo plugin isn't installed please edit "core.editor" below.
# You can set the editor to your choosing, however some functions
# will insist on vim (see 'jev' function below). Edit these to
# your taste.
# WARNING! These functions have only been tested on a local repo!
# I am unsure how well any of this works if pushed to remote (to-do).
# To-Do:
# test functionality with remote origin
# git_log grep for multiple words while searching journal entries
# do not add user and email to config if ~/.gitconfig is already populated
# safely attach notes to individual entries using git_notes
# if "master" journal (branch) is deleted, create new and make default
# present option to export entire journal prior to deletion
# option to export entire journal elsewhere??
# --MAIN--
# where do we store this thing? ideally a directory with nothing else
# in it, especially not an existing git repository!
# (We don't test for this, so use common sense when setting the location.)
#
# NOTE: The JOURNAL env variable can be explicitly set
# (perhaps in .bashrc) to override below setting:
# eg. export JOURNAL=/path/to/elswhere ; source git_journal
JOURNAL="${JOURNAL:=${HOME}/.journal}"
[[ ! -d "${JOURNAL}" ]] && mkdir -p "${JOURNAL}"
LOCKDIR="/tmp/.journal_git_config.exclusivelock"
# various git env variables; these are NECESSARY
INITOPTS="--quiet --bare ${JOURNAL}"
GITOPTS="--git-dir=${JOURNAL} --work-tree=${JOURNAL}"
COMMITOPTS="--quiet --allow-empty"
# not so necessary, but nice
LOGOPTS="--pretty=tformat:%Cgreen%ar %Cblue%s%Creset %<(40,trunc)%b"
UGLYLOG="--pretty=tformat:%CgreenDate: %ad%nEntry: %h%n%Cred%n%w(80,4,4)%s%n%Creset%b"
VIMVIEW="--pretty=tformat:Date: %ad%nEntry: %h%n%n%s%n%n%b"
# initialize the repository, with first journal named "master";
# you still need to create the first entry,
# however, this can be done at any time using either 'jec' or 'jei' functions.
git init ${INITOPTS}
rc="$?"
if [[ "${rc}" -eq "0" ]]; then
# WARNING! Two (or more) shells that source this git_journal file at the SAME TIME
# (screen, tmux, etc) CAN create a git_config race condition, and one shell
# will complain about a config.lock. This is a known git_config bug.
#
# We use mkdir to guard against this, since it is atomic in Linux.
# (flock didn't work after much effort...)
# Function to remove the lock directory
function cleanup() {
if rmdir ${LOCKDIR}; then
echo "Finished"
else
echo "Failed to remove lock directory '${LOCKDIR}'"
return 1
fi
}
if mkdir ${LOCKDIR} 2>&1 >/dev/null; then
# Ensure that if we "grabbed a lock", we release it
# Works for SIGTERM and SIGINT(Ctrl-C)
trap "cleanup" EXIT
# Do the work...
# add useful git options to git_config
#
# [core]
git config --file=${JOURNAL}/config --replace-all core.shared "0600"
# set git journal editor, with spellcheck ON
git config --file=${JOURNAL}/config --replace-all core.editor "vim -c Goyo -c startinsert -c 'set spell spelllang=en_us'"
# same but without spell check
#git config --file=${JOURNAL}/config --replace-all core.editor "vim -c Goyo -c startinsert"
# ... but what if we don't have Goyo plugin?
#git config --file=${JOURNAL}/config --replace-all core.editor "vim -c startinsert"
#git config --file=${JOURNAL}/config --replace-all core.editor "other_editor"
# these would be nice IF you use git_notes AND you push/pull from remote
# otherwise your git_notes namespace(s) will NOT be synced!
git config --file=${JOURNAL}/config --replace-all core.fetch "+refs/notes/*:refs/notes/*"
git config --file=${JOURNAL}/config --replace-all core.push "+refs/notes/*:refs/notes/*"
# [commit]
# git commit is sooo noisy - quell that nonsense...
git config --file=${JOURNAL}/config --replace-all commit.status false
git config --file=${JOURNAL}/config --replace-all commit.showUntrackedFiles no
# [user]
# create a dummy user for this repo, in case we don't have a ~/.gitconfig defined
# to-do: optionally skip this if ~/.gitconfig is already populated with these values
#git config --file=${JOURNAL}/config user.name "Journal on $(hostname)"
#git config --file=${JOURNAL}/config user.email "journal@$(hostname)"
git config --file=${JOURNAL}/config user.name "Journal on Journal"
git config --file=${JOURNAL}/config user.email "journal@journal"
# [log]
git config --file=${JOURNAL}/config --replace-all log.date local
else
# Something went wrong...
echo "Could not create lock directory '${LOCKDIR}'"
return 1
fi
# nifty git_journalfunctions
#
function jh() { # JOURNAL HELP
# show a helpful message
echo -e "\nVarious functions to aid in creating a git based journal:\n"
echo -e "\tjec: create a new journal entry"
echo -e "\tjei <arg>: import a journal entry from text file"
echo -e "\tjex <arg>: export a journal entry to text file"
echo -e "\tjed: delete the most recent entry"
echo -e "\tjev <arg>: view a solitary journal entry"
echo -e "\tjea: edit the most recent journal entry"
echo -e "\tjv: view journal entries in a simple way"
echo -e "\tjvv: view journal entries in an expanded way"
echo -e "\tjf <arg>: search journal entries for <arg>"
echo -e "\tjbc <arg>: create a separate journal named <arg>"
echo -e "\tjbl: list all available journals"
echo -e "\tjbs <arg>: switch to journal named <arg>"
echo -e "\tjbd <arg>: delete a journal named <arg>"
echo -e "\tjh: show this helpful message\n"
}
function jec() { # JOURNAL ENTRY CREATE
# create a journal entry
# argument is optional;
# however adding an argument enclosed
# by quotes will create a "quick" journal entry
# eg. jec "this is an entry"
if [[ -z ${1} ]] ; then
git ${GITOPTS} commit ${COMMITOPTS}
else
git ${GITOPTS} commit ${COMMITOPTS} -m "${1}"
fi
# clear COMMIT_EDITMSG file
:>${JOURNAL}/COMMIT_EDITMSG
}
function jei() { # JOURNAL ENTRY IMPORT
# create a journal entry from an imported text file
# assumes an argument!
# eg. jei /path/to/file
if [ -z "${1}" ] || [ ! -f "${1}" ] ; then
echo "You must provide a file name to import."
return 255
elif [[ ! $( file -i "${1}" | grep -i ascii ) ]] ; then
echo "File ${1} does not appear to be an ASCII file; try again."
return 255
else
cat "${1}" | git ${GITOPTS} commit ${COMMITOPTS} --file=-
# bizarrely, this more obvious construct does NOT work...!
#git ${GITOPTS} commit ${COMMITOPTS} --file="${1}"
if [[ ${?} -ne 0 ]] ; then
echo "Unable to import file as journal entry."
return 1
else
echo "Journal entry imported."
fi
fi
# clear COMMIT_EDITMSG file
:>${JOURNAL}/COMMIT_EDITMSG
}
function jex() { # JOURNAL ENTRY EXPORT
# export a solitary journal entry
# assumes an argument!
# takes the entry number (commit hash) as argument
# eg. jex 1234567
# ... you can find this value using 'jvv' function
if [[ -z ${1} ]] ; then
echo "You must provide the entry number as an argument."
return
else
# we need to test if this commit SHA actually exists!
git ${GITOPTS} cat-file -e "${1}" &>/dev/null
if [[ ${?} -eq 0 ]] ; then
# create a rational name for the exported file
cur_branch=$(git ${GITOPTS} branch -a | grep \* | cut -d ' ' -f2)
exported_file="${HOME}/exported_${cur_branch}_entry-${1}.txt"
# export to file
git --no-pager ${GITOPTS} log "${VIMVIEW}" -n 1 "${1}" > "${exported_file}"
if [[ ${?} -eq 0 ]] ; then
echo "Journal entry exported to ${exported_file}"
else
echo "Unable to export journal entry."
return 1
fi
else
# you did not provide a commit SHA that exists in this branch
echo "Journal entry number provided is invalid."
return 128
fi
fi
}
function jed() { # JOURNAL ENTRY DELETE
# delete the most recent entry in the journal
# this cannot be undone!
echo -e "Do you want to delete the most recent entry?"
read -s -t 10 -n 1 -p "This action CANNOT be undone! Proceed? (y/n)" answer
# NOTE: this prompt will timeout after 10 seconds, defaulting to "No"
case ${answer:0:1} in
y|Y )
git ${GITOPTS} reset --hard HEAD^ &>/dev/null
echo -e "\nJournal entry deleted."
;;
* )
echo ""
;;
esac
}
function jev() { # JOURNAL ENTRY VIEW
# view a solitary journal entry
# assumes an argument!
# takes the entry number (commit hash) as argument
# eg. jev 1234567
# ... you can find this value using 'jvv' function
if [[ -z ${1} ]] ; then
echo "You must provide the entry number as an argument."
return
else
# we need to test if this commit SHA actually exists!
git ${GITOPTS} cat-file -e "${1}" &>/dev/null
if [[ "${?}" -eq 0 ]] ; then
# we use vim here; edit to taste
git --no-pager ${GITOPTS} log "${VIMVIEW}" -n 1 "${1}" | vim -R -c Goyo -
else
echo "Journal entry number provided is invalid."
return 128
fi
fi
}
function jea() { # JOURNAL ENTRY AMEND
# edit the most recent journal entry
git ${GITOPTS} commit ${COMMITOPTS} --amend
# clear COMMIT_EDITMSG file
:>${JOURNAL}/COMMIT_EDITMSG
}
function jv() { # JOURNAL VIEW, SHORT
# view entries in a simple way
git ${GITOPTS} log "${LOGOPTS}"
}
function jvv() { # JOURNAL VIEW, VERBOSE
# ...or more verbosely
git ${GITOPTS} log "${UGLYLOG}"
}
function jf() { # JOURNAL FIND
# search journal entries for a word
# assumes an argument!
# to-do: git_log grep for multiple words
if [[ -z ${1} ]]; then
echo "You must provide a single word to search for!"
echo "eg: jf Wellington"
return
else
git init ${INITOPTS} && git ${GITOPTS} log "${UGLYLOG}" --grep="${1}"
fi
}
function jbc() { # JOURNAL BRANCH CREATE
# create a new, separate journal in the same repository;
# eg. work, personal, hobbies
# ...think of this the same as a "5-Subject Notebook":
# ie. One journal but multiple separate sections.
#
# assumes an argument!
if [[ -z ${1} ]]; then
echo "You must provide the name of your new journal!"
echo "eg: jbc recipes"
return
fi
git ${GITOPTS} checkout --orphan "${1}"
git ${GITOPTS} commit ${COMMITOPTS} -m "This is a journal named ${1}" -m "You can only edit this entry before creating new entries!"
git --no-pager ${GITOPTS} log --pretty=tformat:'%Cgreen%ad %Cblue%s%Creset %b'
read -s -t 10 -n 1 -p "Do you want to edit this entry now? (y/n)" answer
# NOTE: this prompt will timeout after 10 seconds, defaulting to "No"
case ${answer:0:1} in
y|Y)
git ${GITOPTS} commit ${COMMITOPTS} --amend
# clear COMMIT_EDITMSG file
:>${JOURNAL}/COMMIT_EDITMSG
;;
*)
echo ""
;;
esac
}
function jbl() { # JOURNAL BRANCH LIST
# list all journals
echo "Your journals:"
git --no-pager ${GITOPTS} branch -a
echo ""
}
function jbs() { # JOURNAL BRANCH SWITCH
# switch between created journals
# assumes an argument!
if [[ -z ${1} ]]; then
echo "You must provide the journal name to switch TO!"
echo "eg: jbs recipes"
#git --no-pager ${GITOPTS} branch -a
#echo ""
return
fi
git ${GITOPTS} checkout --quiet "${1}" &>/dev/null
if [[ ${?} -ne 0 ]]; then
echo "Journal ${1} does not exist!"
return
fi
cur_branch=$(git ${GITOPTS} branch -a | grep \* | cut -d ' ' -f2)
echo "You are now in journal ${cur_branch}."
}
function jbd() { # JOURNAL BRANCH DELETE
# delete an entire journal
# WARNING: THIS CANNOT be undone!
# assumes an argument!
if [[ -z ${1} ]]; then
echo "You must provide the journal name to delete!"
echo "eg: jbd recipes"
#git --no-pager ${GITOPTS} branch -a
#echo ""
return
else
echo -e "Do you want to delete the entire journal ${1}?"
read -s -t 10 -n 1 -p "This action CANNOT be undone! Proceed? (y/n)" answer
# NOTE: this prompt will timeout after 10 seconds, defaulting to "No"
case ${answer:0:1} in
y|Y )
git ${GITOPTS} branch -D "${1}" &>/dev/null
if [[ ${?} -ne 0 ]]; then
echo -e "\nUnable to delete ${1}; does it exist?"
else
# journal deletion successful
git ${GITOPTS} checkout --quiet master &>/dev/null
echo -e "\nJournal ${1} has been deleted forever!"
cur_branch=$(git ${GITOPTS} branch -a | grep \* | cut -d ' ' -f2)
echo -e "\nYou are now in journal ${cur_branch}."
return
fi
;;
* )
echo -e "\nJournal ${1} not deleted."
;;
esac
fi
}
else # how did we get here? you could not create
# the journal repository for some reason...
echo "Hmmmmmm..."
return 128
fi
# End Journal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment