#!/usr/bin/env bash # Browse git log through fzf, it will search both commit messages and code. # Anything you can pass to git log you can pass to this script. It also # supports passing in --side to configure delta to use side-by-side mode. # # While looking at the git log you can press CTRL+o to open it in a browser. # # Examples: # gl # Search commit messages # gl -S # Search commit messages and code using git's -S flag # gl -G # Search commit messages and code using git's -G flag # gl --side # Show diffs side by side # gl [any flag that git log supports] set -o errexit set -o pipefail set -o nounset # Later on we'll use this to open / browse the selected commit. open_commit_url() { local commit="${1}" [ -z "${commit}" ] && return local branch= local remote= local remote_url= local normalized_url= branch="$(git branch --show-current)" remote="$(git config --get "branch.${branch}.remote")" remote_url="$(git remote get-url "${remote:-origin}" 2>/dev/null)" # Ensure all remote formats (SSH, HTTPS, variants, etc.) get converted into # an https:// URL without .git since this is what will get opened. normalized_url="$(sed -E 's|^(git@\|https?://\|ssh://git@)([:/]+)?||; s|\.git/?$||; s|:|/|; s|^|https://|; s|/+|/|g; s|https:/|https://|; s|/$||' <<<"${remote_url}")" local commit_path= case "${normalized_url}" in *gitlab*) commit_path="-/commit/${commit}" ;; *bitbucket*) commit_path="commits/${commit}" ;; *) commit_path="commit/${commit}" ;; esac local commit_url="${normalized_url}/${commit_path}" if [[ "${OSTYPE}" == linux* ]]; then xdg-open "${commit_url}" >/dev/null 2>&1 else open "${commit_url}" >/dev/null 2>&1 fi } export -f open_commit_url if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then GIT_ARGS=("--color=always" "--date=short") GIT_LOG_SEARCH_MODE="--grep" for arg in "${@}"; do if [ "${arg}" = "--side" ]; then # Handle displaying side by side. export DELTA_FEATURES="${DELTA_FEATURES:-} side-by-side" elif [ "${arg}" = "-S" ]; then GIT_LOG_SEARCH_MODE="-S" elif [ "${arg}" = "-G" ]; then GIT_LOG_SEARCH_MODE="-G" else GIT_ARGS+=("${arg}") fi done printf -v GIT_ARGS_STR "%q " "${GIT_ARGS[@]}" # %at is a hidden timestamp, it it used for sorting later. # COLOR is a placeholder that changes depending on the search result type. FORMAT="%at %C(magenta)%h %C(green)%ad %C(COLOR)> %C(reset)%s" FORMAT_MESSAGE="${FORMAT//COLOR/blue}" FORMAT_CODE="${FORMAT//COLOR/yellow}" STRIP_TIMESTAMP="cut -d ' ' -f 2-" GIT_LOG_DEFAULT="git log --pretty='${FORMAT_MESSAGE}' ${GIT_ARGS_STR:-} | ${STRIP_TIMESTAMP}" # There's a lot going on here, here's a few comments for my future self: # # start:reload # Run this when fzf starts, this works instead of piping in git log because # we also use --disabled. # # change:reload:sleep # change fires every time a key is pressed # reload starts a new process with the typed query # sleep acts as a 150ms debounce to trigger the change / reload # # [[ \"\$query\" =~ [A-Z] ]] && flag=\"\" || flag=\"--regexp-ignore-case\" # This is effectively smart casing, if any capital letters exist then the # search is case sensitive, otherwise it's case insensitive. # # --grep and -G run as separate searches to make them an OR instead of AND, # we run them in a subshell to sort their results later. We always want newest # commits on top, sorted by the hidden timestamp and we use a different > color # to know if it's a message (blue) or code (yellow) result. # # The CTRL+o bind is running through bash explicitly because when fzf triggers # execute, it will use your system's shell and exporting a function like we're # doing doesn't work in zsh, so we're forcing bash. # # If you're sniping this script without DotFriedRice, you'll likely want to # set a number of FZF_DEFAULT_OPTS such as the ones you can find here: # https://github.com/nickjj/dotfriedrice/blob/master/.config/fzf/config.sh fzf \ --ansi --border=top --border-label " [ Enter: Show (local) | CTRL-o: Open (browser) | CTRL-y: Copy (commit) ] " \ --disabled --no-sort --query "" \ --bind "start:reload(${GIT_LOG_DEFAULT})" \ --bind "change:reload:sleep 0.15 query={q} if [ -z \"\${query}\" ]; then ${GIT_LOG_DEFAULT} else [[ \"\${query}\" =~ [A-Z] ]] && case= || case=\"--regexp-ignore-case\" { if [ \"${GIT_LOG_SEARCH_MODE}\" = \"--grep\" ]; then git log \${case} --pretty='${FORMAT_MESSAGE}' ${GIT_ARGS_STR:-} ${GIT_LOG_SEARCH_MODE} \"\${query}\" else ( git log \${case} --pretty='${FORMAT_MESSAGE}' ${GIT_ARGS_STR:-} --grep \"\${query}\" git log \${case} --pretty='${FORMAT_CODE}' ${GIT_ARGS_STR:-} ${GIT_LOG_SEARCH_MODE} \"\${query}\" ) | sort --numeric-sort --reverse --unique --key 1,1 fi } | ${STRIP_TIMESTAMP} fi" \ --bind "enter:execute(git show {1} -- | delta --paging always)" \ --bind "ctrl-o:execute-silent(bash -c 'source ${0}; open_commit_url {1}')" \ --bind "ctrl-y:execute-silent(git show {1} -- | clip-copy)" \ --preview "git show {1} -- | delta --width=\${FZF_PREVIEW_COLUMNS}" \ --preview-window="right:70%" || true fi