1
0
mirror of https://github.com/ohmyzsh/ohmyzsh.git synced 2025-12-12 15:34:50 +08:00

Compare commits

...

4 Commits

5 changed files with 839 additions and 365 deletions

View File

@ -1,360 +1,391 @@
#!/usr/bin/env zsh
function omz {
[[ $# -gt 0 ]] || {
_omz::help
return 1
}
[[ $# -gt 0 ]] || {
_omz::help
return 1
}
local command="$1"
shift
local command="$1"
shift
# Subcommand functions start with _ so that they don't
# appear as completion entries when looking for `omz`
(( $+functions[_omz::$command] )) || {
_omz::help
return 1
}
# Subcommand functions start with _ so that they don't
# appear as completion entries when looking for `omz`
(( $+functions[_omz::$command] )) || {
_omz::help
return 1
}
_omz::$command "$@"
_omz::$command "$@"
}
function _omz {
local -a cmds subcmds
cmds=(
'help:Usage information'
'plugin:Commands for Oh My Zsh plugins management'
'pr:Commands for Oh My Zsh Pull Requests management'
'theme:Commands for Oh My Zsh themes management'
'update:Update Oh My Zsh'
)
local -a cmds subcmds
cmds=(
'changelog:Print the changelog'
'help:Usage information'
'plugin:Manage plugins'
'pr:Manage Oh My Zsh Pull Requests'
'theme:Manage themes'
'update:Update Oh My Zsh'
)
if (( CURRENT == 2 )); then
_describe 'command' cmds
elif (( CURRENT == 3 )); then
case "$words[2]" in
plugin) subcmds=('list:List plugins')
_describe 'command' subcmds ;;
pr) subcmds=('test:Test a Pull Request' 'clean:Delete all Pull Request branches')
_describe 'command' subcmds ;;
theme) subcmds=('use:Load a theme' 'list:List themes')
_describe 'command' subcmds ;;
esac
elif (( CURRENT == 4 )); then
case "$words[2]::$words[3]" in
theme::use) compadd "$ZSH"/themes/*.zsh-theme(.N:t:r) \
"$ZSH_CUSTOM"/**/*.zsh-theme(.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::) ;;
esac
fi
if (( CURRENT == 2 )); then
_describe 'command' cmds
elif (( CURRENT == 3 )); then
case "$words[2]" in
changelog) local -a refs
refs=("${(@f)$(command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}")
_describe 'command' refs ;;
plugin) subcmds=('list:List plugins')
_describe 'command' subcmds ;;
pr) subcmds=('test:Test a Pull Request' 'clean:Delete all Pull Request branches')
_describe 'command' subcmds ;;
theme) subcmds=('use:Load a theme' 'list:List themes')
_describe 'command' subcmds ;;
esac
elif (( CURRENT == 4 )); then
case "$words[2]::$words[3]" in
theme::use) compadd "$ZSH"/themes/*.zsh-theme(.N:t:r) \
"$ZSH_CUSTOM"/**/*.zsh-theme(.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::) ;;
esac
fi
return 0
return 0
}
compdef _omz omz
## Utility functions
function _omz::confirm {
# If question supplied, ask it before reading the answer
# NOTE: uses the logname of the caller function
if [[ -n "$1" ]]; then
_omz::log prompt "$1" "${${functrace[1]#_}%:*}"
fi
# Read one character
read -r -k 1
# If no newline entered, add a newline
if [[ "$REPLY" != $'\n' ]]; then
echo
fi
}
function _omz::log {
# if promptsubst is set, a message with `` or $()
# will be run even if quoted due to `print -P`
setopt localoptions nopromptsubst
# $1 = info|warn|error|debug
# $2 = text
# $3 = (optional) name of the logger
local logtype=$1
local logname=${3:-${${functrace[1]#_}%:*}}
# Don't print anything if debug is not active
if [[ $logtype = debug && -z $_OMZ_DEBUG ]]; then
return
fi
# Choose coloring based on log type
case "$logtype" in
prompt) print -Pn "%S%F{blue}$logname%f%s: $2" ;;
debug) print -P "%F{white}$logname%f: $2" ;;
info) print -P "%F{green}$logname%f: $2" ;;
warn) print -P "%S%F{yellow}$logname%f%s: $2" ;;
error) print -P "%S%F{red}$logname%f%s: $2" ;;
esac >&2
}
## User-facing commands
function _omz::help {
cat <<EOF
cat <<EOF
Usage: omz <command> [options]
Available commands:
help Print this help message
plugin <command> Manage plugins
pr <command> Manage Oh My Zsh Pull Requests
theme <command> Manage themes
update Update Oh My Zsh
help Print this help message
changelog Print the changelog
plugin <command> Manage plugins
pr <command> Manage Oh My Zsh Pull Requests
theme <command> Manage themes
update Update Oh My Zsh
EOF
}
function _omz::confirm {
# If question supplied, ask it before reading the answer
# NOTE: uses the logname of the caller function
if [[ -n "$1" ]]; then
_omz::log prompt "$1" "${${functrace[1]#_}%:*}"
fi
function _omz::changelog {
local version=${1:-HEAD} format=${3:-"--text"}
# Read one character
read -r -k 1
if ! command git -C "$ZSH" show-ref --verify refs/heads/$version &>/dev/null && \
! command git -C "$ZSH" show-ref --verify refs/tags/$version &>/dev/null && \
! command git -C "$ZSH" rev-parse --verify "${version}^{commit}" &>/dev/null; then
cat <<EOF
Usage: omz changelog [version]
# If no newline entered, add a newline
if [[ "$REPLY" != $'\n' ]]; then
echo
fi
}
NOTE: <version> must be a valid branch, tag or commit.
EOF
return 1
fi
function _omz::log {
# if promptsubst is set, a message with `` or $()
# will be run even if quoted due to `print -P`
setopt localoptions nopromptsubst
# $1 = info|warn|error|debug
# $2 = text
# $3 = (optional) name of the logger
local logtype=$1
local logname=${3:-${${functrace[1]#_}%:*}}
# Don't print anything if debug is not active
if [[ $logtype = debug && -z $_OMZ_DEBUG ]]; then
return
fi
# Choose coloring based on log type
case "$logtype" in
prompt) print -Pn "%S%F{blue}$logname%f%s: $2" ;;
debug) print -P "%F{white}$logname%f: $2" ;;
info) print -P "%F{green}$logname%f: $2" ;;
warn) print -P "%S%F{yellow}$logname%f%s: $2" ;;
error) print -P "%S%F{red}$logname%f%s: $2" ;;
esac >&2
"$ZSH/tools/changelog.sh" "$version" "${2:-}" "$format"
}
function _omz::plugin {
(( $# > 0 && $+functions[_omz::plugin::$1] )) || {
cat <<EOF
(( $# > 0 && $+functions[_omz::plugin::$1] )) || {
cat <<EOF
Usage: omz plugin <command> [options]
Available commands:
list List all available Oh My Zsh plugins
list List all available Oh My Zsh plugins
EOF
return 1
}
return 1
}
local command="$1"
shift
local command="$1"
shift
_omz::plugin::$command "$@"
_omz::plugin::$command "$@"
}
function _omz::plugin::list {
local -a custom_plugins builtin_plugins
custom_plugins=("$ZSH_CUSTOM"/plugins/*(-/N:t))
builtin_plugins=("$ZSH"/plugins/*(-/N:t))
local -a custom_plugins builtin_plugins
custom_plugins=("$ZSH_CUSTOM"/plugins/*(-/N:t))
builtin_plugins=("$ZSH"/plugins/*(-/N:t))
# If the command is being piped, print all found line by line
if [[ ! -t 1 ]]; then
print -l ${(q-)custom_plugins} ${(q-)builtin_plugins}
return
fi
# If the command is being piped, print all found line by line
if [[ ! -t 1 ]]; then
print -l ${(q-)custom_plugins} ${(q-)builtin_plugins}
return
fi
if (( ${#custom_plugins} )); then
print -P "%U%BCustom plugins%b%u:"
print -l ${(q-)custom_plugins} | column
fi
if (( ${#custom_plugins} )); then
print -P "%U%BCustom plugins%b%u:"
print -l ${(q-)custom_plugins} | column
fi
if (( ${#builtin_plugins} )); then
(( ${#custom_plugins} )) && echo # add a line of separation
if (( ${#builtin_plugins} )); then
(( ${#custom_plugins} )) && echo # add a line of separation
print -P "%U%BBuilt-in plugins%b%u:"
print -l ${(q-)builtin_plugins} | column
fi
print -P "%U%BBuilt-in plugins%b%u:"
print -l ${(q-)builtin_plugins} | column
fi
}
function _omz::pr {
(( $# > 0 && $+functions[_omz::pr::$1] )) || {
cat <<EOF
(( $# > 0 && $+functions[_omz::pr::$1] )) || {
cat <<EOF
Usage: omz pr <command> [options]
Available commands:
clean Delete all PR branches (ohmyzsh/pull-*)
test <PR_number_or_URL> Fetch PR #NUMBER and rebase against master
clean Delete all PR branches (ohmyzsh/pull-*)
test <PR_number_or_URL> Fetch PR #NUMBER and rebase against master
EOF
return 1
}
return 1
}
local command="$1"
shift
local command="$1"
shift
_omz::pr::$command "$@"
_omz::pr::$command "$@"
}
function _omz::pr::clean {
(
set -e
builtin cd -q "$ZSH"
(
set -e
builtin cd -q "$ZSH"
# Check if there are PR branches
local fmt branches
fmt="%(color:bold blue)%(align:18,right)%(refname:short)%(end)%(color:reset) %(color:dim bold red)%(objectname:short)%(color:reset) %(color:yellow)%(contents:subject)"
branches="$(command git for-each-ref --sort=-committerdate --color --format="$fmt" "refs/heads/ohmyzsh/pull-*")"
# Check if there are PR branches
local fmt branches
fmt="%(color:bold blue)%(align:18,right)%(refname:short)%(end)%(color:reset) %(color:dim bold red)%(objectname:short)%(color:reset) %(color:yellow)%(contents:subject)"
branches="$(command git for-each-ref --sort=-committerdate --color --format="$fmt" "refs/heads/ohmyzsh/pull-*")"
# Exit if there are no PR branches
if [[ -z "$branches" ]]; then
_omz::log info "there are no Pull Request branches to remove."
return
fi
# Print found PR branches
echo "$branches\n"
# Confirm before removing the branches
_omz::confirm "do you want remove these Pull Request branches? [Y/n] "
# Only proceed if the answer is a valid yes option
[[ "$REPLY" != [yY$'\n'] ]] && return
_omz::log info "removing all Oh My Zsh Pull Request branches..."
command git branch --list 'ohmyzsh/pull-*' | while read branch; do
command git branch -D "$branch"
done
)
}
function _omz::pr::test {
# Allow $1 to be a URL to the pull request
if [[ "$1" = https://* ]]; then
1="${1:t}"
# Exit if there are no PR branches
if [[ -z "$branches" ]]; then
_omz::log info "there are no Pull Request branches to remove."
return
fi
# Check the input
if ! [[ -n "$1" && "$1" =~ ^[[:digit:]]+$ ]]; then
echo >&2 "Usage: omz pr test <PR_NUMBER_or_URL>"
return 1
fi
# Save current git HEAD
local branch
branch=$(builtin cd -q "$ZSH"; git symbolic-ref --short HEAD) || {
_omz::log error "error when getting the current git branch. Aborting..."
return 1
}
# Fetch PR onto ohmyzsh/pull-<PR_NUMBER> branch and rebase against master
# If any of these operations fail, undo the changes made
(
set -e
builtin cd -q "$ZSH"
# Get the ohmyzsh git remote
command git remote -v | while read remote url _; do
case "$url" in
https://github.com/ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
git@github.com:ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
esac
done
(( $found )) || {
_omz::log error "could not found the ohmyzsh git remote. Aborting..."
return 1
}
# Fetch pull request head
_omz::log info "fetching PR #$1 to ohmyzsh/pull-$1..."
command git fetch -f "$remote" refs/pull/$1/head:ohmyzsh/pull-$1 || {
_omz::log error "error when trying to fetch PR #$1."
return 1
}
# Rebase pull request branch against the current master
_omz::log info "rebasing PR #$1..."
command git rebase master ohmyzsh/pull-$1 || {
command git rebase --abort &>/dev/null
_omz::log warn "could not rebase PR #$1 on top of master."
_omz::log warn "you might not see the latest stable changes."
_omz::log info "run \`zsh\` to test the changes."
return 1
}
_omz::log info "fetch of PR #${1} successful."
)
# If there was an error, abort running zsh to test the PR
[[ $? -eq 0 ]] || return 1
# Run zsh to test the changes
_omz::log info "running \`zsh\` to test the changes. Run \`exit\` to go back."
command zsh -l
# After testing, go back to the previous HEAD if the user wants
_omz::confirm "do you want to go back to the previous branch? [Y/n] "
# Print found PR branches
echo "$branches\n"
# Confirm before removing the branches
_omz::confirm "do you want remove these Pull Request branches? [Y/n] "
# Only proceed if the answer is a valid yes option
[[ "$REPLY" != [yY$'\n'] ]] && return
(
set -e
builtin cd -q "$ZSH"
_omz::log info "removing all Oh My Zsh Pull Request branches..."
command git branch --list 'ohmyzsh/pull-*' | while read branch; do
command git branch -D "$branch"
done
)
}
command git checkout "$branch" -- || {
_omz::log error "could not go back to the previous branch ('$branch')."
return 1
}
)
function _omz::pr::test {
# Allow $1 to be a URL to the pull request
if [[ "$1" = https://* ]]; then
1="${1:t}"
fi
# Check the input
if ! [[ -n "$1" && "$1" =~ ^[[:digit:]]+$ ]]; then
echo >&2 "Usage: omz pr test <PR_NUMBER_or_URL>"
return 1
fi
# Save current git HEAD
local branch
branch=$(builtin cd -q "$ZSH"; git symbolic-ref --short HEAD) || {
_omz::log error "error when getting the current git branch. Aborting..."
return 1
}
# Fetch PR onto ohmyzsh/pull-<PR_NUMBER> branch and rebase against master
# If any of these operations fail, undo the changes made
(
set -e
builtin cd -q "$ZSH"
# Get the ohmyzsh git remote
command git remote -v | while read remote url _; do
case "$url" in
https://github.com/ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
git@github.com:ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
esac
done
(( $found )) || {
_omz::log error "could not found the ohmyzsh git remote. Aborting..."
return 1
}
# Fetch pull request head
_omz::log info "fetching PR #$1 to ohmyzsh/pull-$1..."
command git fetch -f "$remote" refs/pull/$1/head:ohmyzsh/pull-$1 || {
_omz::log error "error when trying to fetch PR #$1."
return 1
}
# Rebase pull request branch against the current master
_omz::log info "rebasing PR #$1..."
command git rebase master ohmyzsh/pull-$1 || {
command git rebase --abort &>/dev/null
_omz::log warn "could not rebase PR #$1 on top of master."
_omz::log warn "you might not see the latest stable changes."
_omz::log info "run \`zsh\` to test the changes."
return 1
}
_omz::log info "fetch of PR #${1} successful."
)
# If there was an error, abort running zsh to test the PR
[[ $? -eq 0 ]] || return 1
# Run zsh to test the changes
_omz::log info "running \`zsh\` to test the changes. Run \`exit\` to go back."
command zsh -l
# After testing, go back to the previous HEAD if the user wants
_omz::confirm "do you want to go back to the previous branch? [Y/n] "
# Only proceed if the answer is a valid yes option
[[ "$REPLY" != [yY$'\n'] ]] && return
(
set -e
builtin cd -q "$ZSH"
command git checkout "$branch" -- || {
_omz::log error "could not go back to the previous branch ('$branch')."
return 1
}
)
}
function _omz::theme {
(( $# > 0 && $+functions[_omz::theme::$1] )) || {
cat <<EOF
(( $# > 0 && $+functions[_omz::theme::$1] )) || {
cat <<EOF
Usage: omz theme <command> [options]
Available commands:
list List all available Oh My Zsh themes
use <theme> Load an Oh My Zsh theme
list List all available Oh My Zsh themes
use <theme> Load an Oh My Zsh theme
EOF
return 1
}
return 1
}
local command="$1"
shift
local command="$1"
shift
_omz::theme::$command "$@"
_omz::theme::$command "$@"
}
function _omz::theme::list {
local -a custom_themes builtin_themes
custom_themes=("$ZSH_CUSTOM"/**/*.zsh-theme(.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
builtin_themes=("$ZSH"/themes/*.zsh-theme(.N:t:r))
local -a custom_themes builtin_themes
custom_themes=("$ZSH_CUSTOM"/**/*.zsh-theme(.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
builtin_themes=("$ZSH"/themes/*.zsh-theme(.N:t:r))
# If the command is being piped, print all found line by line
if [[ ! -t 1 ]]; then
print -l ${(q-)custom_themes} ${(q-)builtin_themes}
return
fi
# If the command is being piped, print all found line by line
if [[ ! -t 1 ]]; then
print -l ${(q-)custom_themes} ${(q-)builtin_themes}
return
fi
if (( ${#custom_themes} )); then
print -P "%U%BCustom themes%b%u:"
print -l ${(q-)custom_themes} | column
fi
if (( ${#custom_themes} )); then
print -P "%U%BCustom themes%b%u:"
print -l ${(q-)custom_themes} | column
fi
if (( ${#builtin_themes} )); then
(( ${#custom_themes} )) && echo # add a line of separation
if (( ${#builtin_themes} )); then
(( ${#custom_themes} )) && echo # add a line of separation
print -P "%U%BBuilt-in themes%b%u:"
print -l ${(q-)builtin_themes} | column
fi
print -P "%U%BBuilt-in themes%b%u:"
print -l ${(q-)builtin_themes} | column
fi
}
function _omz::theme::use {
if [[ -z "$1" ]]; then
echo >&2 "Usage: omz theme use <theme>"
return 1
fi
if [[ -z "$1" ]]; then
echo >&2 "Usage: omz theme use <theme>"
return 1
fi
# Respect compatibility with old lookup order
if [[ -f "$ZSH_CUSTOM/$1.zsh-theme" ]]; then
source "$ZSH_CUSTOM/$1.zsh-theme"
elif [[ -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]]; then
source "$ZSH_CUSTOM/themes/$1.zsh-theme"
elif [[ -f "$ZSH/themes/$1.zsh-theme" ]]; then
source "$ZSH/themes/$1.zsh-theme"
else
_omz::log error "theme '$1' not found"
return 1
fi
# Respect compatibility with old lookup order
if [[ -f "$ZSH_CUSTOM/$1.zsh-theme" ]]; then
source "$ZSH_CUSTOM/$1.zsh-theme"
elif [[ -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]]; then
source "$ZSH_CUSTOM/themes/$1.zsh-theme"
elif [[ -f "$ZSH/themes/$1.zsh-theme" ]]; then
source "$ZSH/themes/$1.zsh-theme"
else
_omz::log error "theme '$1' not found"
return 1
fi
}
function _omz::update {
# Run update script
env ZSH="$ZSH" sh "$ZSH/tools/upgrade.sh"
# Update last updated file
zmodload zsh/datetime
echo "LAST_EPOCH=$(( EPOCHSECONDS / 60 / 60 / 24 ))" >! "${ZSH_CACHE_DIR}/.zsh-update"
# Remove update lock if it exists
command rm -rf "$ZSH/log/update.lock"
# Run update script
env ZSH="$ZSH" zsh -f "$ZSH/tools/upgrade.sh"
local ret=$?
# Update last updated file
zmodload zsh/datetime
echo "LAST_EPOCH=$(( EPOCHSECONDS / 60 / 60 / 24 ))" >! "${ZSH_CACHE_DIR}/.zsh-update"
# Remove update lock if it exists
command rm -rf "$ZSH/log/update.lock"
# Restart the zsh session
if [[ $ret -eq 0 ]]; then
# Check whether to run a login shell
[[ "$ZSH_ARGZERO" = -* ]] && exec -l "${ZSH_ARGZERO#-}" || exec "$ZSH_ARGZERO"
fi
}

View File

@ -14,12 +14,18 @@ function upgrade_oh_my_zsh() {
fi
# Run update script
env ZSH="$ZSH" sh "$ZSH/tools/upgrade.sh"
env ZSH="$ZSH" zsh -f "$ZSH/tools/upgrade.sh"
local ret=$?
# Update last updated file
zmodload zsh/datetime
echo "LAST_EPOCH=$(( EPOCHSECONDS / 60 / 60 / 24 ))" >! "${ZSH_CACHE_DIR}/.zsh-update"
# Remove update lock if it exists
command rm -rf "$ZSH/log/update.lock"
# Restart the zsh session
if [[ $ret -eq 0 ]]; then
# Check whether to run a login shell
[[ "$ZSH_ARGZERO" = -* ]] && exec -l "${ZSH_ARGZERO#-}" || exec "$ZSH_ARGZERO"
fi
}
function take() {

418
tools/changelog.sh Executable file
View File

@ -0,0 +1,418 @@
#!/usr/bin/env zsh
##############################
# CHANGELOG SCRIPT CONSTANTS #
##############################
#* Holds the list of valid types recognized in a commit subject
#* and the display string of such type
local -A TYPES
TYPES=(
[build]="Build system"
[chore]="Chore"
[ci]="CI"
[docs]="Documentation"
[feat]="Features"
[fix]="Bug fixes"
[perf]="Performance"
[refactor]="Refactor"
[style]="Style"
[test]="Testing"
)
#* Types that will be displayed in their own section,
#* in the order specified here.
local -a MAIN_TYPES
MAIN_TYPES=(feat fix perf docs)
#* Types that will be displayed under the category of other changes
local -a OTHER_TYPES
OTHER_TYPES=(refactor style other)
#* Commit types that don't appear in $MAIN_TYPES nor $OTHER_TYPES
#* will not be displayed and will simply be ignored.
############################
# COMMIT PARSING UTILITIES #
############################
function parse-commit {
# This function uses the following globals as output: commits (A),
# subjects (A), scopes (A) and breaking (A). All associative arrays (A)
# have $hash as the key.
# - commits holds the commit type
# - subjects holds the commit subject
# - scopes holds the scope of a commit
# - breaking holds the breaking change warning if a commit does
# make a breaking change
function commit:type {
local type="$(sed -E 's/^([a-zA-Z_\-]+)(\(.+\))?!?: .+$/\1/' <<< "$1")"
# If $type doesn't appear in $TYPES array mark it as 'other'
if [[ -n "${(k)TYPES[(i)$type]}" ]]; then
echo $type
else
echo other
fi
}
function commit:scope {
local scope
# Try to find scope in "type(<scope>):" format
scope=$(sed -nE 's/^[a-zA-Z_\-]+\((.+)\)!?: .+$/\1/p' <<< "$1")
if [[ -n "$scope" ]]; then
echo "$scope"
return
fi
# If no scope found, try to find it in "<scope>:" format
# Make sure it's not a type before printing it
scope=$(sed -nE 's/^([a-zA-Z_\-]+): .+$/\1/p' <<< "$1")
if [[ -z "${(k)TYPES[(i)$scope]}" ]]; then
echo "$scope"
fi
}
function commit:subject {
# Only display the relevant part of the commit, i.e. if it has the format
# type[(scope)!]: subject, where the part between [] is optional, only
# displays subject. If it doesn't match the format, returns the whole string.
sed -E 's/^[a-zA-Z_\-]+(\(.+\))?!?: (.+)$/\2/' <<< "$1"
}
# Return subject if the body or subject match the breaking change format
function commit:is-breaking {
local subject="$1" body="$2"
if [[ "$body" =~ "BREAKING CHANGE: (.*)" || \
"$subject" =~ '^[^ :\)]+\)?!: (.*)$' ]]; then
echo "${match[1]}"
else
return 1
fi
}
# Return truncated hash of the reverted commit
function commit:is-revert {
local subject="$1" body="$2"
if [[ "$subject" = Revert* && \
"$body" =~ "This reverts commit ([^.]+)\." ]]; then
echo "${match[1]:0:7}"
else
return 1
fi
}
# Parse commit with hash $1
local hash="$1" subject body warning rhash
subject="$(command git show -s --format=%s $hash)"
body="$(command git show -s --format=%b $hash)"
# Commits following Conventional Commits (https://www.conventionalcommits.org/)
# have the following format, where parts between [] are optional:
#
# type[(scope)][!]: subject
#
# commit body
# [BREAKING CHANGE: warning]
# commits holds the commit type
commits[$hash]="$(commit:type "$subject")"
# scopes holds the commit scope
scopes[$hash]="$(commit:scope "$subject")"
# subjects holds the commit subject
subjects[$hash]="$(commit:subject "$subject")"
# breaking holds whether a commit has breaking changes
# and its warning message if it does
if warning=$(commit:is-breaking "$subject" "$body"); then
breaking[$hash]="$warning"
fi
# reverts holds commits reverted in the same release
if rhash=$(commit:is-revert "$subject" "$body"); then
reverts[$hash]=$rhash
fi
}
#############################
# RELEASE CHANGELOG DISPLAY #
#############################
function display-release {
# This function uses the following globals: output, version,
# commits (A), subjects (A), scopes (A), breaking (A) and reverts (A).
#
# - output is the output format to use when formatting (raw|text|md)
# - version is the version in which the commits are made
# - commits, subjects, scopes, breaking, and reverts are associative arrays
# with commit hashes as keys
# Remove commits that were reverted
local hash rhash
for hash rhash in ${(kv)reverts}; do
if (( ${+commits[$rhash]} )); then
# Remove revert commit
unset "commits[$hash]" "subjects[$hash]" "scopes[$hash]" "breaking[$hash]"
# Remove reverted commit
unset "commits[$rhash]" "subjects[$rhash]" "scopes[$rhash]" "breaking[$rhash]"
fi
done
# If no commits left skip displaying the release
if (( $#commits == 0 )); then
return
fi
##* Formatting functions
# Format the hash according to output format
# If no parameter is passed, assume it comes from `$hash`
function fmt:hash {
#* Uses $hash from outer scope
local hash="${1:-$hash}"
case "$output" in
raw) printf "$hash" ;;
text) printf "\e[33m$hash\e[0m" ;; # red
md) printf "[\`$hash\`](https://github.com/ohmyzsh/ohmyzsh/commit/$hash)" ;;
esac
}
# Format headers according to output format
# Levels 1 to 2 are considered special, the rest are formatted
# the same, except in md output format.
function fmt:header {
local header="$1" level="$2"
case "$output" in
raw)
case "$level" in
1) printf "$header\n$(printf '%.0s=' {1..${#header}})\n\n" ;;
2) printf "$header\n$(printf '%.0s-' {1..${#header}})\n\n" ;;
*) printf "$header:\n\n" ;;
esac ;;
text)
case "$level" in
1|2) printf "\e[1;4m$header\e[0m\n\n" ;; # bold, underlined
*) printf "\e[1m$header:\e[0m\n\n" ;; # bold
esac ;;
md) printf "$(printf '%.0s#' {1..${level}}) $header\n\n" ;;
esac
}
function fmt:scope {
#* Uses $scopes (A) and $hash from outer scope
local scope="${1:-${scopes[$hash]}}"
# Get length of longest scope for padding
local max_scope=0 padding=0
for hash in ${(k)scopes}; do
max_scope=$(( max_scope < ${#scopes[$hash]} ? ${#scopes[$hash]} : max_scope ))
done
# If no scopes, exit the function
if [[ $max_scope -eq 0 ]]; then
return
fi
# Get how much padding is required for this scope
padding=$(( max_scope < ${#scope} ? 0 : max_scope - ${#scope} ))
padding="${(r:$padding:: :):-}"
# If no scope, print padding and 3 spaces (equivalent to "[] ")
if [[ -z "$scope" ]]; then
printf "${padding} "
return
fi
# Print [scope]
case "$output" in
raw|md) printf "[$scope]${padding} " ;;
text) printf "[\e[38;5;9m$scope\e[0m]${padding} " ;; # red 9
esac
}
# If no parameter is passed, assume it comes from `$subjects[$hash]`
function fmt:subject {
#* Uses $subjects (A) and $hash from outer scope
local subject="${1:-${subjects[$hash]}}"
# Capitalize first letter of the subject
subject="${(U)subject:0:1}${subject:1}"
case "$output" in
raw) printf "$subject" ;;
# In text mode, highlight (#<issue>) and dim text between `backticks`
text) sed -E $'s|#([0-9]+)|\e[32m#\\1\e[0m|g;s|`(.+)`|`\e[2m\\1\e[0m`|g' <<< "$subject" ;;
# In markdown mode, link to (#<issue>) issues
md) sed -E 's|#([0-9]+)|[#\1](https://github.com/ohmyzsh/ohmyzsh/issues/\1)|g' <<< "$subject" ;;
esac
}
function fmt:type {
#* Uses $type from outer scope
local type="${1:-${TYPES[$type]:-${(C)type}}}"
[[ -z "$type" ]] && return 0
case "$output" in
raw|md) printf "$type: " ;;
text) printf "\e[4m$type\e[24m: " ;; # underlined
esac
}
##* Section functions
function display:version {
fmt:header "$version" 2
}
function display:breaking {
(( $#breaking != 0 )) || return 0
case "$output" in
raw) display:type-header "BREAKING CHANGES" ;;
text|md) display:type-header "⚠ BREAKING CHANGES" ;;
esac
local hash subject
for hash message in ${(kv)breaking}; do
echo " - $(fmt:hash) $(fmt:subject "${message}")"
done | sort
echo
}
function display:type {
local hash type="$1"
local -a hashes
hashes=(${(k)commits[(R)$type]})
# If no commits found of type $type, go to next type
(( $#hashes != 0 )) || return 0
fmt:header "${TYPES[$type]}" 3
for hash in $hashes; do
echo " - $(fmt:hash) $(fmt:scope)$(fmt:subject)"
done | sort -k3 # sort by scope
echo
}
function display:others {
local hash type
# Commits made under types considered other changes
local -A changes
changes=(${(kv)commits[(R)${(j:|:)OTHER_TYPES}]})
# If no commits found under "other" types, don't display anything
(( $#changes != 0 )) || return 0
fmt:header "Other changes" 3
for hash type in ${(kv)changes}; do
case "$type" in
other) echo " - $(fmt:hash) $(fmt:scope)$(fmt:subject)" ;;
*) echo " - $(fmt:hash) $(fmt:scope)$(fmt:type)$(fmt:subject)" ;;
esac
done | sort -k3 # sort by scope
echo
}
##* Release sections order
# Display version header
display:version
# Display breaking changes first
display:breaking
# Display changes for commit types in the order specified
for type in $MAIN_TYPES; do
display:type "$type"
done
# Display other changes
display:others
}
function main {
# $1 = until commit, $2 = since commit
# $3 = output format (--raw|--text|--md)
local until="$1" since="$2"
local output=${${3:-"--text"}#--*}
if [[ -z "$until" ]]; then
until=HEAD
fi
# If $since is not specified, look up first version tag before $until
if [[ -z "$since" ]]; then
since=$(command git describe --abbrev=0 --tags "$until^" 2>/dev/null) || \
unset since
elif [[ "$since" = --all ]]; then
unset since
fi
# Commit classification arrays
local -A commits subjects scopes breaking reverts
local truncate=0 read_commits=0
local hash version tag
# Get the first version name:
# 1) try tag-like version, or
# 2) try name-rev, or
# 3) try branch name, or
# 4) try short hash
version=$(command git describe --tags $until 2>/dev/null) \
|| version=$(command git name-rev --no-undefined --name-only --exclude="remotes/*" $until 2>/dev/null) \
|| version=$(command git symbolic-ref --quiet --short $until 2>/dev/null) \
|| version=$(command git rev-parse --short $until 2>/dev/null)
# Get commit list from $until commit until $since commit, or until root
# commit if $since is unset, in short hash form.
# --first-parent is used when dealing with merges: it only prints the
# merge commit, not the commits of the merged branch.
command git rev-list --first-parent --abbrev-commit --abbrev=7 ${since:+$since..}$until | while read hash; do
# Truncate list on versions with a lot of commits
if [[ -z "$since" ]] && (( ++read_commits > 35 )); then
truncate=1
break
fi
# If we find a new release (exact tag)
if tag=$(command git describe --exact-match --tags $hash 2>/dev/null); then
# Output previous release
display-release
# Reinitialize commit storage
commits=()
subjects=()
scopes=()
breaking=()
reverts=()
# Start work on next release
version="$tag"
read_commits=1
fi
parse-commit "$hash"
done
display-release
if (( truncate )); then
echo " ...more commits omitted"
echo
fi
}
cd "$ZSH"
# Use raw output if stdout is not a tty
if [[ ! -t 1 && -z "$3" ]]; then
main "$1" "$2" --raw
else
main "$@"
fi

View File

@ -1,6 +1,6 @@
# Migrate .zsh-update file to $ZSH_CACHE_DIR
if [[ -f ~/.zsh-update && ! -f "${ZSH_CACHE_DIR}/.zsh-update" ]]; then
mv ~/.zsh-update "${ZSH_CACHE_DIR}/.zsh-update"
mv ~/.zsh-update "${ZSH_CACHE_DIR}/.zsh-update"
fi
# Cancel update if:
@ -10,79 +10,81 @@ fi
if [[ "$DISABLE_AUTO_UPDATE" = true ]] \
|| [[ ! -w "$ZSH" || ! -O "$ZSH" ]] \
|| ! command -v git &>/dev/null; then
return
return
fi
function current_epoch() {
zmodload zsh/datetime
echo $(( EPOCHSECONDS / 60 / 60 / 24 ))
zmodload zsh/datetime
echo $(( EPOCHSECONDS / 60 / 60 / 24 ))
}
function update_last_updated_file() {
echo "LAST_EPOCH=$(current_epoch)" >! "${ZSH_CACHE_DIR}/.zsh-update"
echo "LAST_EPOCH=$(current_epoch)" >! "${ZSH_CACHE_DIR}/.zsh-update"
}
function update_ohmyzsh() {
ZSH="$ZSH" sh "$ZSH/tools/upgrade.sh"
update_last_updated_file
ZSH="$ZSH" zsh -f "$ZSH/tools/upgrade.sh"
update_last_updated_file
}
() {
emulate -L zsh
emulate -L zsh
local epoch_target mtime option LAST_EPOCH
local epoch_target mtime option LAST_EPOCH
# Remove lock directory if older than a day
zmodload zsh/datetime
zmodload -F zsh/stat b:zstat
if mtime=$(zstat +mtime "$ZSH/log/update.lock" 2>/dev/null); then
if (( (mtime + 3600 * 24) < EPOCHSECONDS )); then
command rm -rf "$ZSH/log/update.lock"
fi
# Remove lock directory if older than a day
zmodload zsh/datetime
zmodload -F zsh/stat b:zstat
if mtime=$(zstat +mtime "$ZSH/log/update.lock" 2>/dev/null); then
if (( (mtime + 3600 * 24) < EPOCHSECONDS )); then
command rm -rf "$ZSH/log/update.lock"
fi
fi
# Check for lock directory
if ! command mkdir "$ZSH/log/update.lock" 2>/dev/null; then
return
fi
# Check for lock directory
if ! command mkdir "$ZSH/log/update.lock" 2>/dev/null; then
return
fi
# Remove lock directory on exit. `return 1` is important for when trapping a SIGINT:
# The return status from the function is handled specially. If it is zero, the signal is
# assumed to have been handled, and execution continues normally. Otherwise, the shell
# will behave as interrupted except that the return status of the trap is retained.
trap "command rm -rf '$ZSH/log/update.lock'; return 1" EXIT INT QUIT
# Remove lock directory on exit. `return 1` is important for when trapping a SIGINT:
# The return status from the function is handled specially. If it is zero, the signal is
# assumed to have been handled, and execution continues normally. Otherwise, the shell
# will behave as interrupted except that the return status of the trap is retained.
trap "
unset -f current_epoch update_last_updated_file update_ohmyzsh
command rm -rf '$ZSH/log/update.lock'
return 1
" EXIT INT QUIT
# Create or update .zsh-update file if missing or malformed
if ! source "${ZSH_CACHE_DIR}/.zsh-update" 2>/dev/null || [[ -z "$LAST_EPOCH" ]]; then
update_last_updated_file
return
fi
# Create or update .zsh-update file if missing or malformed
if ! source "${ZSH_CACHE_DIR}/.zsh-update" 2>/dev/null || [[ -z "$LAST_EPOCH" ]]; then
update_last_updated_file
return
fi
# Number of days before trying to update again
epoch_target=${UPDATE_ZSH_DAYS:-13}
# Test if enough time has passed until the next update
if (( ( $(current_epoch) - $LAST_EPOCH ) < $epoch_target )); then
return
fi
# Number of days before trying to update again
epoch_target=${UPDATE_ZSH_DAYS:-13}
# Test if enough time has passed until the next update
if (( ( $(current_epoch) - $LAST_EPOCH ) < $epoch_target )); then
return
fi
# Ask for confirmation before updating unless disabled
if [[ "$DISABLE_UPDATE_PROMPT" = true ]]; then
update_ohmyzsh
else
# input sink to swallow all characters typed before the prompt
# and add a newline if there wasn't one after characters typed
while read -t -k 1 option; do true; done
[[ "$option" != ($'\n'|"") ]] && echo
# Ask for confirmation before updating unless disabled
if [[ "$DISABLE_UPDATE_PROMPT" = true ]]; then
update_ohmyzsh
else
# input sink to swallow all characters typed before the prompt
# and add a newline if there wasn't one after characters typed
while read -t -k 1 option; do true; done
[[ "$option" != ($'\n'|"") ]] && echo
echo -n "[oh-my-zsh] Would you like to update? [Y/n] "
read -r -k 1 option
[[ "$option" != $'\n' ]] && echo
case "$option" in
[yY$'\n']) update_ohmyzsh ;;
[nN]) update_last_updated_file ;;
esac
fi
echo -n "[oh-my-zsh] Would you like to update? [Y/n] "
read -r -k 1 option
[[ "$option" != $'\n' ]] && echo
case "$option" in
[yY$'\n']) update_ohmyzsh ;;
[nN]) update_last_updated_file ;;
esac
fi
}
unset -f current_epoch update_last_updated_file update_ohmyzsh

105
tools/upgrade.sh Normal file → Executable file
View File

@ -1,13 +1,23 @@
# Use colors, but only if connected to a terminal, and that terminal
# supports them.
#!/usr/bin/env zsh
cd "$ZSH"
# Use colors, but only if connected to a terminal
# and that terminal supports them.
local -a RAINBOW
local RED GREEN YELLOW BLUE UNDER BOLD RESET
if [ -t 1 ]; then
RB_RED=$(printf '\033[38;5;196m')
RB_ORANGE=$(printf '\033[38;5;202m')
RB_YELLOW=$(printf '\033[38;5;226m')
RB_GREEN=$(printf '\033[38;5;082m')
RB_BLUE=$(printf '\033[38;5;021m')
RB_INDIGO=$(printf '\033[38;5;093m')
RB_VIOLET=$(printf '\033[38;5;163m')
RAINBOW=(
"$(printf '\033[38;5;196m')"
"$(printf '\033[38;5;202m')"
"$(printf '\033[38;5;226m')"
"$(printf '\033[38;5;082m')"
"$(printf '\033[38;5;021m')"
"$(printf '\033[38;5;093m')"
"$(printf '\033[38;5;163m')"
)
RED=$(printf '\033[31m')
GREEN=$(printf '\033[32m')
@ -16,25 +26,19 @@ if [ -t 1 ]; then
BOLD=$(printf '\033[1m')
UNDER=$(printf '\033[4m')
RESET=$(printf '\033[m')
else
RB_RED=""
RB_ORANGE=""
RB_YELLOW=""
RB_GREEN=""
RB_BLUE=""
RB_INDIGO=""
RB_VIOLET=""
RED=""
GREEN=""
YELLOW=""
BLUE=""
UNDER=""
BOLD=""
RESET=""
fi
cd "$ZSH"
# Update upstream remote to ohmyzsh org
git remote -v | while read remote url _; do
case "$url" in
https://github.com/robbyrussell/oh-my-zsh(|.git))
git remote set-url "$remote" "https://github.com/ohmyzsh/ohmyzsh.git"
break ;;
git@github.com:robbyrussell/oh-my-zsh(|.git))
git remote set-url "$remote" "git@github.com:ohmyzsh/ohmyzsh.git"
break ;;
esac
done
# Set git-config values known to fix git errors
# Line endings (#4069)
@ -45,30 +49,43 @@ git config fsck.zeroPaddedFilemode ignore
git config fetch.fsck.zeroPaddedFilemode ignore
git config receive.fsck.zeroPaddedFilemode ignore
# autostash on rebase (#7172)
resetAutoStash=$(git config --bool rebase.autoStash 2>&1)
resetAutoStash=$(git config --bool rebase.autoStash 2>/dev/null)
git config rebase.autoStash true
# Update upstream remote to ohmyzsh org
remote=$(git remote -v | awk '/https:\/\/github\.com\/robbyrussell\/oh-my-zsh\.git/{ print $1; exit }')
if [ -n "$remote" ]; then
git remote set-url "$remote" "https://github.com/ohmyzsh/ohmyzsh.git"
fi
local ret=0
# Update Oh My Zsh
printf "${BLUE}%s${RESET}\n" "Updating Oh My Zsh"
if git pull --rebase --stat origin master
then
printf '%s %s__ %s %s %s %s %s__ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
printf '%s ____ %s/ /_ %s ____ ___ %s__ __ %s ____ %s_____%s/ /_ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
printf '%s / __ \%s/ __ \ %s / __ `__ \%s/ / / / %s /_ / %s/ ___/%s __ \ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
printf '%s/ /_/ /%s / / / %s / / / / / /%s /_/ / %s / /_%s(__ )%s / / / %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
printf '%s\____/%s_/ /_/ %s /_/ /_/ /_/%s\__, / %s /___/%s____/%s_/ /_/ %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
printf '%s %s %s %s /____/ %s %s %s %s\n' $RB_RED $RB_ORANGE $RB_YELLOW $RB_GREEN $RB_BLUE $RB_INDIGO $RB_VIOLET $RB_RESET
printf "${BLUE}%s\n" "Hooray! Oh My Zsh has been updated and/or is at the current version."
printf "${BLUE}${BOLD}%s ${UNDER}%s${RESET}\n" "To keep up on the latest news and updates, follow us on Twitter:" "https://twitter.com/ohmyzsh"
last_commit=$(git rev-parse HEAD)
if git pull --rebase --stat origin master; then
# Check if it was really updated or not
if [[ "$(git rev-parse HEAD)" = "$last_commit" ]]; then
message="Oh My Zsh is already at the latest version."
ret=80 # non-zero exit code to indicate no changes pulled
else
message="Hooray! Oh My Zsh has been updated!"
# Display changelog with less if available, otherwise just print it to the terminal
if (( $+commands[less] )); then
command less -R <("$ZSH/tools/changelog.sh" HEAD "$last_commit")
else
"$ZSH/tools/changelog.sh" HEAD "$last_commit"
fi
fi
printf '%s %s__ %s %s %s %s %s__ %s\n' $RAINBOW $RESET
printf '%s ____ %s/ /_ %s ____ ___ %s__ __ %s ____ %s_____%s/ /_ %s\n' $RAINBOW $RESET
printf '%s / __ \%s/ __ \ %s / __ `__ \%s/ / / / %s /_ / %s/ ___/%s __ \ %s\n' $RAINBOW $RESET
printf '%s/ /_/ /%s / / / %s / / / / / /%s /_/ / %s / /_%s(__ )%s / / / %s\n' $RAINBOW $RESET
printf '%s\____/%s_/ /_/ %s /_/ /_/ /_/%s\__, / %s /___/%s____/%s_/ /_/ %s\n' $RAINBOW $RESET
printf '%s %s %s %s /____/ %s %s %s %s\n' $RAINBOW $RESET
printf '\n'
printf "${BLUE}%s${RESET}\n" "$message"
printf "${BLUE}${BOLD}%s ${UNDER}%s${RESET}\n" "To keep up with the latest news and updates, follow us on Twitter:" "https://twitter.com/ohmyzsh"
printf "${BLUE}${BOLD}%s ${UNDER}%s${RESET}\n" "Want to get involved in the community? Join our Discord:" "https://discord.gg/ohmyzsh"
printf "${BLUE}${BOLD}%s ${UNDER}%s${RESET}\n" "Get your Oh My Zsh swag at:" "https://shop.planetargon.com/collections/oh-my-zsh"
else
status=$?
ret=$?
printf "${RED}%s${RESET}\n" 'There was an error updating. Try again later?'
fi
@ -79,4 +96,4 @@ case "$resetAutoStash" in
esac
# Exit with `1` if the update failed
exit $status
exit $ret