Parent directory

cli.zsh

27390 bytes
  1#!/usr/bin/env zsh
  2
  3function omz {
  4  setopt localoptions noksharrays
  5  [[ $# -gt 0 ]] || {
  6    _omz::help
  7    return 1
  8  }
  9
 10  local command="$1"
 11  shift
 12
 13  # Subcommand functions start with _ so that they don't
 14  # appear as completion entries when looking for `omz`
 15  (( ${+functions[_omz::$command]} )) || {
 16    _omz::help
 17    return 1
 18  }
 19
 20  _omz::$command "$@"
 21}
 22
 23function _omz {
 24  local -a cmds subcmds
 25  cmds=(
 26    'changelog:Print the changelog'
 27    'help:Usage information'
 28    'plugin:Manage plugins'
 29    'pr:Manage Oh My Zsh Pull Requests'
 30    'reload:Reload the current zsh session'
 31    'shop:Open the Oh My Zsh shop'
 32    'theme:Manage themes'
 33    'update:Update Oh My Zsh'
 34    'version:Show the version'
 35  )
 36
 37  if (( CURRENT == 2 )); then
 38    _describe 'command' cmds
 39  elif (( CURRENT == 3 )); then
 40    case "$words[2]" in
 41      changelog) local -a refs
 42        refs=("${(@f)$(builtin cd -q "$ZSH"; command git for-each-ref --format="%(refname:short):%(subject)" refs/heads refs/tags)}")
 43        _describe 'command' refs ;;
 44      plugin) subcmds=(
 45        'disable:Disable plugin(s)'
 46        'enable:Enable plugin(s)'
 47        'info:Get plugin information'
 48        'list:List plugins'
 49        'load:Load plugin(s)'
 50      )
 51        _describe 'command' subcmds ;;
 52      pr) subcmds=('clean:Delete all Pull Request branches' 'test:Test a Pull Request')
 53        _describe 'command' subcmds ;;
 54      theme) subcmds=('list:List themes' 'set:Set a theme in your .zshrc file' 'use:Load a theme')
 55        _describe 'command' subcmds ;;
 56    esac
 57  elif (( CURRENT == 4 )); then
 58    case "${words[2]}::${words[3]}" in
 59      plugin::(disable|enable|load))
 60        local -aU valid_plugins
 61
 62        if [[ "${words[3]}" = disable ]]; then
 63          # if command is "disable", only offer already enabled plugins
 64          valid_plugins=($plugins)
 65        else
 66          valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
 67          # if command is "enable", remove already enabled plugins
 68          [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
 69        fi
 70
 71        _describe 'plugin' valid_plugins ;;
 72      plugin::info)
 73        local -aU plugins
 74        plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
 75        _describe 'plugin' plugins ;;
 76      plugin::list)
 77        local -a opts
 78        opts=('--enabled:List enabled plugins only')
 79        _describe -o 'options' opts ;;
 80      theme::(set|use))
 81        local -aU themes
 82        themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r) "$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
 83        _describe 'theme' themes ;;
 84    esac
 85  elif (( CURRENT > 4 )); then
 86    case "${words[2]}::${words[3]}" in
 87      plugin::(enable|disable|load))
 88        local -aU valid_plugins
 89
 90        if [[ "${words[3]}" = disable ]]; then
 91          # if command is "disable", only offer already enabled plugins
 92          valid_plugins=($plugins)
 93        else
 94          valid_plugins=("$ZSH"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t) "$ZSH_CUSTOM"/plugins/*/{_*,*.plugin.zsh}(-.N:h:t))
 95          # if command is "enable", remove already enabled plugins
 96          [[ "${words[3]}" = enable ]] && valid_plugins=(${valid_plugins:|plugins})
 97        fi
 98
 99        # Remove plugins already passed as arguments
100        # NOTE: $(( CURRENT - 1 )) is the last plugin argument completely passed, i.e. that which
101        # has a space after them. This is to avoid removing plugins partially passed, which makes
102        # the completion not add a space after the completed plugin.
103        local -a args
104        args=(${words[4,$(( CURRENT - 1))]})
105        valid_plugins=(${valid_plugins:|args})
106
107        _describe 'plugin' valid_plugins ;;
108    esac
109  fi
110
111  return 0
112}
113
114# If run from a script, do not set the completion function
115if (( ${+functions[compdef]} )); then
116  compdef _omz omz
117fi
118
119## Utility functions
120
121function _omz::confirm {
122  # If question supplied, ask it before reading the answer
123  # NOTE: uses the logname of the caller function
124  if [[ -n "$1" ]]; then
125    _omz::log prompt "$1" "${${functrace[1]#_}%:*}"
126  fi
127
128  # Read one character
129  read -r -k 1
130
131  # If no newline entered, add a newline
132  if [[ "$REPLY" != $'\n' ]]; then
133    echo
134  fi
135}
136
137function _omz::log {
138  # if promptsubst is set, a message with `` or $()
139  # will be run even if quoted due to `print -P`
140  setopt localoptions nopromptsubst
141
142  # $1 = info|warn|error|debug
143  # $2 = text
144  # $3 = (optional) name of the logger
145
146  local logtype=$1
147  local logname=${3:-${${functrace[1]#_}%:*}}
148
149  # Don't print anything if debug is not active
150  if [[ $logtype = debug && -z $_OMZ_DEBUG ]]; then
151    return
152  fi
153
154  # Choose coloring based on log type
155  case "$logtype" in
156    prompt) print -Pn "%S%F{blue}$logname%f%s: $2" ;;
157    debug) print -P "%F{white}$logname%f: $2" ;;
158    info) print -P "%F{green}$logname%f: $2" ;;
159    warn) print -P "%S%F{yellow}$logname%f%s: $2" ;;
160    error) print -P "%S%F{red}$logname%f%s: $2" ;;
161  esac >&2
162}
163
164## User-facing commands
165
166function _omz::help {
167  cat >&2 <<EOF
168Usage: omz <command> [options]
169
170Available commands:
171
172  help                Print this help message
173  changelog           Print the changelog
174  plugin <command>    Manage plugins
175  pr     <command>    Manage Oh My Zsh Pull Requests
176  reload              Reload the current zsh session
177  shop                Open the Oh My Zsh shop
178  theme  <command>    Manage themes
179  update              Update Oh My Zsh
180  version             Show the version
181
182EOF
183}
184
185function _omz::changelog {
186  local version=${1:-HEAD} format=${3:-"--text"}
187
188  if (
189    builtin cd -q "$ZSH"
190    ! command git show-ref --verify refs/heads/$version && \
191    ! command git show-ref --verify refs/tags/$version && \
192    ! command git rev-parse --verify "${version}^{commit}"
193  ) &>/dev/null; then
194    cat >&2 <<EOF
195Usage: ${(j: :)${(s.::.)0#_}} [version]
196
197NOTE: <version> must be a valid branch, tag or commit.
198EOF
199    return 1
200  fi
201
202  ZSH="$ZSH" command zsh -f "$ZSH/tools/changelog.sh" "$version" "${2:-}" "$format"
203}
204
205function _omz::plugin {
206  (( $# > 0 && $+functions[$0::$1] )) || {
207    cat >&2 <<EOF
208Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
209
210Available commands:
211
212  disable <plugin> Disable plugin(s)
213  enable <plugin>  Enable plugin(s)
214  info <plugin>    Get information of a plugin
215  list [--enabled] List Oh My Zsh plugins
216  load <plugin>    Load plugin(s)
217
218EOF
219    return 1
220  }
221
222  local command="$1"
223  shift
224
225  $0::$command "$@"
226}
227
228function _omz::plugin::disable {
229  if [[ -z "$1" ]]; then
230    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin> [...]"
231    return 1
232  fi
233
234  # Check that plugin is in $plugins
235  local -a dis_plugins
236  for plugin in "$@"; do
237    if [[ ${plugins[(Ie)$plugin]} -eq 0 ]]; then
238      _omz::log warn "plugin '$plugin' is not enabled."
239      continue
240    fi
241    dis_plugins+=("$plugin")
242  done
243
244  # Exit if there are no enabled plugins to disable
245  if [[ ${#dis_plugins} -eq 0 ]]; then
246    return 1
247  fi
248
249  # Remove plugins substitution awk script
250  local awk_subst_plugins="\
251  gsub(/[ \t]+(${(j:|:)dis_plugins})[ \t]+/, \" \") # with spaces before or after
252  gsub(/[ \t]+(${(j:|:)dis_plugins})$/, \"\")       # with spaces before and EOL
253  gsub(/^(${(j:|:)dis_plugins})[ \t]+/, \"\")       # with BOL and spaces after
254
255  gsub(/\((${(j:|:)dis_plugins})[ \t]+/, \"(\")     # with parenthesis before and spaces after
256  gsub(/[ \t]+(${(j:|:)dis_plugins})\)/, \")\")     # with spaces before or parenthesis after
257  gsub(/\((${(j:|:)dis_plugins})\)/, \"()\")        # with only parentheses
258
259  gsub(/^(${(j:|:)dis_plugins})\)/, \")\")          # with BOL and closing parenthesis
260  gsub(/\((${(j:|:)dis_plugins})$/, \"(\")          # with opening parenthesis and EOL
261"
262
263  # Disable plugins awk script
264  local awk_script="
265# if plugins=() is in oneline form, substitute disabled plugins and go to next line
266/^[ \t]*plugins=\([^#]+\).*\$/ {
267  $awk_subst_plugins
268  print \$0
269  next
270}
271
272# if plugins=() is in multiline form, enable multi flag and disable plugins if they're there
273/^[ \t]*plugins=\(/ {
274  multi=1
275  $awk_subst_plugins
276  print \$0
277  next
278}
279
280# if multi flag is enabled and we find a valid closing parenthesis, remove plugins and disable multi flag
281multi == 1 && /^[^#]*\)/ {
282  multi=0
283  $awk_subst_plugins
284  print \$0
285  next
286}
287
288multi == 1 && length(\$0) > 0 {
289  $awk_subst_plugins
290  if (length(\$0) > 0) print \$0
291  next
292}
293
294{ print \$0 }
295"
296
297  local zdot="${ZDOTDIR:-$HOME}"
298  local zshrc="${${:-"${zdot}/.zshrc"}:A}"
299  awk "$awk_script" "$zshrc" > "$zdot/.zshrc.new" \
300  && command cp -f "$zshrc" "$zdot/.zshrc.bck" \
301  && command mv -f "$zdot/.zshrc.new" "$zshrc"
302
303  # Exit if the new .zshrc file wasn't created correctly
304  [[ $? -eq 0 ]] || {
305    local ret=$?
306    _omz::log error "error disabling plugins."
307    return $ret
308  }
309
310  # Exit if the new .zshrc file has syntax errors
311  if ! command zsh -n "$zdot/.zshrc"; then
312    _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
313    command mv -f "$zdot/.zshrc.bck" "$zshrc"
314    return 1
315  fi
316
317  # Restart the zsh session if there were no errors
318  _omz::log info "plugins disabled: ${(j:, :)dis_plugins}."
319
320  # Only reload zsh if run in an interactive session
321  [[ ! -o interactive ]] || _omz::reload
322}
323
324function _omz::plugin::enable {
325  if [[ -z "$1" ]]; then
326    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin> [...]"
327    return 1
328  fi
329
330  # Check that plugin is not in $plugins
331  local -a add_plugins
332  for plugin in "$@"; do
333    if [[ ${plugins[(Ie)$plugin]} -ne 0 ]]; then
334      _omz::log warn "plugin '$plugin' is already enabled."
335      continue
336    fi
337    add_plugins+=("$plugin")
338  done
339
340  # Exit if there are no plugins to enable
341  if [[ ${#add_plugins} -eq 0 ]]; then
342    return 1
343  fi
344
345  # Enable plugins awk script
346  local awk_script="
347# if plugins=() is in oneline form, substitute ) with new plugins and go to the next line
348/^[ \t]*plugins=\([^#]+\).*\$/ {
349  sub(/\)/, \" $add_plugins&\")
350  print \$0
351  next
352}
353
354# if plugins=() is in multiline form, enable multi flag and indent by default with 2 spaces
355/^[ \t]*plugins=\(/ {
356  multi=1
357  indent=\"  \"
358  print \$0
359  next
360}
361
362# if multi flag is enabled and we find a valid closing parenthesis,
363# add new plugins with proper indent and disable multi flag
364multi == 1 && /^[^#]*\)/ {
365  multi=0
366  split(\"$add_plugins\",p,\" \")
367  for (i in p) {
368    print indent p[i]
369  }
370  print \$0
371  next
372}
373
374# if multi flag is enabled and we didn't find a closing parenthesis,
375# get the indentation level to match when adding plugins
376multi == 1 && /^[^#]*/ {
377  indent=\"\"
378  for (i = 1; i <= length(\$0); i++) {
379    char=substr(\$0, i, 1)
380    if (char == \" \" || char == \"\t\") {
381      indent = indent char
382    } else {
383      break
384    }
385  }
386}
387
388{ print \$0 }
389"
390
391  local zdot="${ZDOTDIR:-$HOME}"
392  local zshrc="${${:-"${zdot}/.zshrc"}:A}"
393  awk "$awk_script" "$zshrc" > "$zdot/.zshrc.new" \
394  && command cp -f "$zshrc" "$zdot/.zshrc.bck" \
395  && command mv -f "$zdot/.zshrc.new" "$zshrc"
396
397  # Exit if the new .zshrc file wasn't created correctly
398  [[ $? -eq 0 ]] || {
399    local ret=$?
400    _omz::log error "error enabling plugins."
401    return $ret
402  }
403
404  # Exit if the new .zshrc file has syntax errors
405  if ! command zsh -n "$zdot/.zshrc"; then
406    _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
407    command mv -f "$zdot/.zshrc.bck" "$zshrc"
408    return 1
409  fi
410
411  # Restart the zsh session if there were no errors
412  _omz::log info "plugins enabled: ${(j:, :)add_plugins}."
413
414  # Only reload zsh if run in an interactive session
415  [[ ! -o interactive ]] || _omz::reload
416}
417
418function _omz::plugin::info {
419  if [[ -z "$1" ]]; then
420    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin>"
421    return 1
422  fi
423
424  local readme
425  for readme in "$ZSH_CUSTOM/plugins/$1/README.md" "$ZSH/plugins/$1/README.md"; do
426    if [[ -f "$readme" ]]; then
427      # If being piped, just cat the README
428      if [[ ! -t 1 ]]; then
429        cat "$readme"
430        return $?
431      fi
432
433      # Enrich the README display depending on the tools we have
434      # - glow: https://github.com/charmbracelet/glow
435      # - bat: https://github.com/sharkdp/bat
436      # - less: typical pager command
437      case 1 in
438        ${+commands[glow]}) glow -p "$readme" ;;
439        ${+commands[bat]}) bat -l md --style plain "$readme" ;;
440        ${+commands[less]}) less "$readme" ;;
441        *) cat "$readme" ;;
442      esac
443      return $?
444    fi
445  done
446
447  if [[ -d "$ZSH_CUSTOM/plugins/$1" || -d "$ZSH/plugins/$1" ]]; then
448    _omz::log error "the '$1' plugin doesn't have a README file"
449  else
450    _omz::log error "'$1' plugin not found"
451  fi
452
453  return 1
454}
455
456function _omz::plugin::list {
457  local -a custom_plugins builtin_plugins
458
459  # If --enabled is provided, only list what's enabled
460  if [[ "$1" == "--enabled" ]]; then
461    local plugin
462    for plugin in "${plugins[@]}"; do
463      if [[ -d "${ZSH_CUSTOM}/plugins/${plugin}" ]]; then
464        custom_plugins+=("${plugin}")
465      elif [[ -d "${ZSH}/plugins/${plugin}" ]]; then
466        builtin_plugins+=("${plugin}")
467      fi
468    done
469  else
470    custom_plugins=("$ZSH_CUSTOM"/plugins/*(-/N:t))
471    builtin_plugins=("$ZSH"/plugins/*(-/N:t))
472  fi
473
474  # If the command is being piped, print all found line by line
475  if [[ ! -t 1 ]]; then
476    print -l ${(q-)custom_plugins} ${(q-)builtin_plugins}
477    return
478  fi
479
480  if (( ${#custom_plugins} )); then
481    print -P "%U%BCustom plugins%b%u:"
482    print -lac ${(q-)custom_plugins}
483  fi
484
485  if (( ${#builtin_plugins} )); then
486    (( ${#custom_plugins} )) && echo # add a line of separation
487
488    print -P "%U%BBuilt-in plugins%b%u:"
489    print -lac ${(q-)builtin_plugins}
490  fi
491}
492
493function _omz::plugin::load {
494  if [[ -z "$1" ]]; then
495    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <plugin> [...]"
496    return 1
497  fi
498
499  local plugin base has_completion=0
500  for plugin in "$@"; do
501    if [[ -d "$ZSH_CUSTOM/plugins/$plugin" ]]; then
502      base="$ZSH_CUSTOM/plugins/$plugin"
503    elif [[ -d "$ZSH/plugins/$plugin" ]]; then
504      base="$ZSH/plugins/$plugin"
505    else
506      _omz::log warn "plugin '$plugin' not found"
507      continue
508    fi
509
510    # Check if its a valid plugin
511    if [[ ! -f "$base/_$plugin" && ! -f "$base/$plugin.plugin.zsh" ]]; then
512      _omz::log warn "'$plugin' is not a valid plugin"
513      continue
514    # It is a valid plugin, add its directory to $fpath unless it is already there
515    elif (( ! ${fpath[(Ie)$base]} )); then
516      fpath=("$base" $fpath)
517    fi
518
519    # Check if it has completion to reload compinit
520    local -a comp_files
521    comp_files=($base/_*(N))
522    has_completion=$(( $#comp_files > 0 ))
523
524    # Load the plugin
525    if [[ -f "$base/$plugin.plugin.zsh" ]]; then
526      source "$base/$plugin.plugin.zsh"
527    fi
528  done
529
530  # If we have completion, we need to reload the completion
531  # We pass -D to avoid generating a new dump file, which would overwrite our
532  # current one for the next session (and we don't want that because we're not
533  # actually enabling the plugins for the next session).
534  # Note that we still have to pass -d "$_comp_dumpfile", so that compinit
535  # doesn't use the default zcompdump location (${ZDOTDIR:-$HOME}/.zcompdump).
536  if (( has_completion )); then
537    compinit -D -d "$_comp_dumpfile"
538  fi
539}
540
541function _omz::pr {
542  (( $# > 0 && $+functions[$0::$1] )) || {
543    cat >&2 <<EOF
544Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
545
546Available commands:
547
548  clean                       Delete all PR branches (ohmyzsh/pull-*)
549  test <PR_number_or_URL>     Fetch PR #NUMBER and rebase against master
550
551EOF
552    return 1
553  }
554
555  local command="$1"
556  shift
557
558  $0::$command "$@"
559}
560
561function _omz::pr::clean {
562  (
563    set -e
564    builtin cd -q "$ZSH"
565
566    # Check if there are PR branches
567    local fmt branches
568    fmt="%(color:bold blue)%(align:18,right)%(refname:short)%(end)%(color:reset) %(color:dim bold red)%(objectname:short)%(color:reset) %(color:yellow)%(contents:subject)"
569    branches="$(command git for-each-ref --sort=-committerdate --color --format="$fmt" "refs/heads/ohmyzsh/pull-*")"
570
571    # Exit if there are no PR branches
572    if [[ -z "$branches" ]]; then
573      _omz::log info "there are no Pull Request branches to remove."
574      return
575    fi
576
577    # Print found PR branches
578    echo "$branches\n"
579    # Confirm before removing the branches
580    _omz::confirm "do you want remove these Pull Request branches? [Y/n] "
581    # Only proceed if the answer is a valid yes option
582    [[ "$REPLY" != [yY$'\n'] ]] && return
583
584    _omz::log info "removing all Oh My Zsh Pull Request branches..."
585    command git branch --list 'ohmyzsh/pull-*' | while read branch; do
586      command git branch -D "$branch"
587    done
588  )
589}
590
591function _omz::pr::test {
592  # Allow $1 to be a URL to the pull request
593  if [[ "$1" = https://* ]]; then
594    1="${1:t}"
595  fi
596
597  # Check the input
598  if ! [[ -n "$1" && "$1" =~ ^[[:digit:]]+$ ]]; then
599    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <PR_NUMBER_or_URL>"
600    return 1
601  fi
602
603  # Save current git HEAD
604  local branch
605  branch=$(builtin cd -q "$ZSH"; git symbolic-ref --short HEAD) || {
606    _omz::log error "error when getting the current git branch. Aborting..."
607    return 1
608  }
609
610
611  # Fetch PR onto ohmyzsh/pull-<PR_NUMBER> branch and rebase against master
612  # If any of these operations fail, undo the changes made
613  (
614    set -e
615    builtin cd -q "$ZSH"
616
617    # Get the ohmyzsh git remote
618    command git remote -v | while read remote url _; do
619      case "$url" in
620      https://github.com/ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
621      git@github.com:ohmyzsh/ohmyzsh(|.git)) found=1; break ;;
622      esac
623    done
624
625    (( $found )) || {
626      _omz::log error "could not find the ohmyzsh git remote. Aborting..."
627      return 1
628    }
629
630    # Check if Pull Request has the "testers needed" label
631    _omz::log info "checking if PR #$1 has the 'testers needed' label..."
632    local pr_json label label_id="MDU6TGFiZWw4NzY1NTkwNA=="
633    pr_json=$(
634      curl -fsSL \
635        -H "Accept: application/vnd.github+json" \
636        -H "X-GitHub-Api-Version: 2022-11-28" \
637        "https://api.github.com/repos/ohmyzsh/ohmyzsh/pulls/$1"
638    )
639
640    if [[ $? -gt 0 || -z "$pr_json" ]]; then
641      _omz::log error "error when trying to fetch PR #$1 from GitHub."
642      return 1
643    fi
644
645    # Check if the label is present with jq or grep
646    if (( $+commands[jq] )); then
647      label="$(command jq ".labels.[] | select(.node_id == \"$label_id\")" <<< "$pr_json")"
648    else
649      label="$(command grep "\"$label_id\"" <<< "$pr_json" 2>/dev/null)"
650    fi
651
652    # If a maintainer hasn't labeled the PR to test, explain the security risk
653    if [[ -z "$label" ]]; then
654      _omz::log warn "PR #$1 does not have the 'testers needed' label. This means that the PR"
655      _omz::log warn "has not been reviewed by a maintainer and may contain malicious code."
656
657      # Ask for explicit confirmation: user needs to type "yes" to continue
658      _omz::log prompt "Do you want to continue testing it? [yes/N] "
659      builtin read -r
660      if [[ "${REPLY:l}" != yes ]]; then
661        _omz::log error "PR test canceled. Please ask a maintainer to review and label the PR."
662        return 1
663      else
664        _omz::log warn "Continuing to check out and test PR #$1. Be careful!"
665      fi
666    fi
667
668    # Fetch pull request head
669    _omz::log info "fetching PR #$1 to ohmyzsh/pull-$1..."
670    command git fetch -f "$remote" refs/pull/$1/head:ohmyzsh/pull-$1 || {
671      _omz::log error "error when trying to fetch PR #$1."
672      return 1
673    }
674
675    # Rebase pull request branch against the current master
676    _omz::log info "rebasing PR #$1..."
677    local ret gpgsign
678    {
679      # Back up commit.gpgsign setting: use --local to get the current repository
680      # setting, not the global one. If --local is not a known option, it will
681      # exit with a 129 status code.
682      gpgsign=$(command git config --local commit.gpgsign 2>/dev/null) || ret=$?
683      [[ $ret -ne 129 ]] || gpgsign=$(command git config commit.gpgsign 2>/dev/null)
684      command git config commit.gpgsign false
685
686      command git rebase master ohmyzsh/pull-$1 || {
687        command git rebase --abort &>/dev/null
688        _omz::log warn "could not rebase PR #$1 on top of master."
689        _omz::log warn "you might not see the latest stable changes."
690        _omz::log info "run \`zsh\` to test the changes."
691        return 1
692      }
693    } always {
694      case "$gpgsign" in
695      "") command git config --unset commit.gpgsign ;;
696      *) command git config commit.gpgsign "$gpgsign" ;;
697      esac
698    }
699
700    _omz::log info "fetch of PR #${1} successful."
701  )
702
703  # If there was an error, abort running zsh to test the PR
704  [[ $? -eq 0 ]] || return 1
705
706  # Run zsh to test the changes
707  _omz::log info "running \`zsh\` to test the changes. Run \`exit\` to go back."
708  command zsh -l
709
710  # After testing, go back to the previous HEAD if the user wants
711  _omz::confirm "do you want to go back to the previous branch? [Y/n] "
712  # Only proceed if the answer is a valid yes option
713  [[ "$REPLY" != [yY$'\n'] ]] && return
714
715  (
716    set -e
717    builtin cd -q "$ZSH"
718
719    command git checkout "$branch" -- || {
720      _omz::log error "could not go back to the previous branch ('$branch')."
721      return 1
722    }
723  )
724}
725
726function _omz::shop {
727  local shop_url="https://commitgoods.com/collections/oh-my-zsh"
728  
729  _omz::log info "Opening Oh My Zsh shop in your browser..."
730  _omz::log info "$shop_url"
731  
732  open_command "$shop_url"
733}
734
735function _omz::reload {
736  # Delete current completion cache
737  command rm -f $_comp_dumpfile $ZSH_COMPDUMP
738
739  # Old zsh versions don't have ZSH_ARGZERO
740  local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
741  # Check whether to run a login shell
742  [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
743}
744
745function _omz::theme {
746  (( $# > 0 && $+functions[$0::$1] )) || {
747    cat >&2 <<EOF
748Usage: ${(j: :)${(s.::.)0#_}} <command> [options]
749
750Available commands:
751
752  list            List all available Oh My Zsh themes
753  set <theme>     Set a theme in your .zshrc file
754  use <theme>     Load a theme
755
756EOF
757    return 1
758  }
759
760  local command="$1"
761  shift
762
763  $0::$command "$@"
764}
765
766function _omz::theme::list {
767  local -a custom_themes builtin_themes
768  custom_themes=("$ZSH_CUSTOM"/**/*.zsh-theme(-.N:r:gs:"$ZSH_CUSTOM"/themes/:::gs:"$ZSH_CUSTOM"/:::))
769  builtin_themes=("$ZSH"/themes/*.zsh-theme(-.N:t:r))
770
771  # If the command is being piped, print all found line by line
772  if [[ ! -t 1 ]]; then
773    print -l ${(q-)custom_themes} ${(q-)builtin_themes}
774    return
775  fi
776
777  # Print theme in use
778  if [[ -n "$ZSH_THEME" ]]; then
779    print -Pn "%U%BCurrent theme%b%u: "
780    [[ $ZSH_THEME = random ]] && echo "$RANDOM_THEME (via random)" || echo "$ZSH_THEME"
781    echo
782  fi
783
784  # Print custom themes if there are any
785  if (( ${#custom_themes} )); then
786    print -P "%U%BCustom themes%b%u:"
787    print -lac ${(q-)custom_themes}
788    echo
789  fi
790
791  # Print built-in themes
792  print -P "%U%BBuilt-in themes%b%u:"
793  print -lac ${(q-)builtin_themes}
794}
795
796function _omz::theme::set {
797  if [[ -z "$1" ]]; then
798    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <theme>"
799    return 1
800  fi
801
802  # Check that theme exists
803  if [[ ! -f "$ZSH_CUSTOM/$1.zsh-theme" ]] \
804    && [[ ! -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]] \
805    && [[ ! -f "$ZSH/themes/$1.zsh-theme" ]]; then
806    _omz::log error "%B$1%b theme not found"
807    return 1
808  fi
809
810  # Enable theme in .zshrc
811  local awk_script='
812!set && /^[ \t]*ZSH_THEME=[^#]+.*$/ {
813  set=1
814  sub(/^[ \t]*ZSH_THEME=[^#]+.*$/, "ZSH_THEME=\"'$1'\" # set by `omz`")
815  print $0
816  next
817}
818
819{ print $0 }
820
821END {
822  # If no ZSH_THEME= line was found, return an error
823  if (!set) exit 1
824}
825'
826
827  local zdot="${ZDOTDIR:-$HOME}"
828  local zshrc="${${:-"${zdot}/.zshrc"}:A}"
829  awk "$awk_script" "$zshrc" > "$zdot/.zshrc.new" \
830  || {
831    # Prepend ZSH_THEME= line to .zshrc if it doesn't exist
832    cat <<EOF
833ZSH_THEME="$1" # set by \`omz\`
834
835EOF
836    cat "$zdot/.zshrc"
837  } > "$zdot/.zshrc.new" \
838  && command cp -f "$zshrc" "$zdot/.zshrc.bck" \
839  && command mv -f "$zdot/.zshrc.new" "$zshrc"
840
841  # Exit if the new .zshrc file wasn't created correctly
842  [[ $? -eq 0 ]] || {
843    local ret=$?
844    _omz::log error "error setting theme."
845    return $ret
846  }
847
848  # Exit if the new .zshrc file has syntax errors
849  if ! command zsh -n "$zdot/.zshrc"; then
850    _omz::log error "broken syntax in '"${zdot/#$HOME/\~}/.zshrc"'. Rolling back changes..."
851    command mv -f "$zdot/.zshrc.bck" "$zshrc"
852    return 1
853  fi
854
855  # Restart the zsh session if there were no errors
856  _omz::log info "'$1' theme set correctly."
857
858  # Only reload zsh if run in an interactive session
859  [[ ! -o interactive ]] || _omz::reload
860}
861
862function _omz::theme::use {
863  if [[ -z "$1" ]]; then
864    echo >&2 "Usage: ${(j: :)${(s.::.)0#_}} <theme>"
865    return 1
866  fi
867
868  # Respect compatibility with old lookup order
869  if [[ -f "$ZSH_CUSTOM/$1.zsh-theme" ]]; then
870    source "$ZSH_CUSTOM/$1.zsh-theme"
871  elif [[ -f "$ZSH_CUSTOM/themes/$1.zsh-theme" ]]; then
872    source "$ZSH_CUSTOM/themes/$1.zsh-theme"
873  elif [[ -f "$ZSH/themes/$1.zsh-theme" ]]; then
874    source "$ZSH/themes/$1.zsh-theme"
875  else
876    _omz::log error "%B$1%b theme not found"
877    return 1
878  fi
879
880  # Update theme settings
881  ZSH_THEME="$1"
882  [[ $1 = random ]] || unset RANDOM_THEME
883}
884
885function _omz::update {
886  # Check if git command is available
887  (( $+commands[git] )) || {
888    _omz::log error "git is not installed. Aborting..."
889    return 1
890  }
891
892  # Check if --unattended was passed
893  [[ "$1" != --unattended ]] || {
894    _omz::log error "the \`\e[2m--unattended\e[0m\` flag is no longer supported, use the \`\e[2mupgrade.sh\e[0m\` script instead."
895    _omz::log error "for more information see https://github.com/ohmyzsh/ohmyzsh/wiki/FAQ#how-do-i-update-oh-my-zsh"
896    return 1
897  }
898
899  local last_commit=$(builtin cd -q "$ZSH"; git rev-parse HEAD 2>/dev/null)
900  [[ $? -eq 0 ]] || {
901    _omz::log error "\`$ZSH\` is not a git directory. Aborting..."
902    return 1
903  }
904
905  # Run update script
906  zstyle -s ':omz:update' verbose verbose_mode || verbose_mode=default
907  ZSH="$ZSH" command zsh -f "$ZSH/tools/upgrade.sh" -i -v $verbose_mode || return $?
908
909  # Update last updated file
910  zmodload zsh/datetime
911  echo "LAST_EPOCH=$(( EPOCHSECONDS / 60 / 60 / 24 ))" >! "${ZSH_CACHE_DIR}/.zsh-update"
912  # Remove update lock if it exists
913  command rm -rf "$ZSH/log/update.lock"
914
915  # Restart the zsh session if there were changes
916  if [[ "$(builtin cd -q "$ZSH"; git rev-parse HEAD)" != "$last_commit" ]]; then
917    # Old zsh versions don't have ZSH_ARGZERO
918    local zsh="${ZSH_ARGZERO:-${functrace[-1]%:*}}"
919    # Check whether to run a login shell
920    [[ "$zsh" = -* || -o login ]] && exec -l "${zsh#-}" || exec "$zsh"
921  fi
922}
923
924function _omz::version {
925  (
926    builtin cd -q "$ZSH"
927
928    # Get the version name:
929    # 1) try tag-like version
930    # 2) try branch name
931    # 3) try name-rev (tag~<rev> or branch~<rev>)
932    local version
933    version=$(command git describe --tags HEAD 2>/dev/null) \
934    || version=$(command git symbolic-ref --quiet --short HEAD 2>/dev/null) \
935    || version=$(command git name-rev --no-undefined --name-only --exclude="remotes/*" HEAD 2>/dev/null) \
936    || version="<detached>"
937
938    # Get short hash for the current HEAD
939    local commit=$(command git rev-parse --short HEAD 2>/dev/null)
940
941    # Show version and commit hash
942    printf "%s (%s)\n" "$version" "$commit"
943  )
944}