Last active
February 7, 2020 17:24
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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