Merge branch 'master' into quote-filenames
This commit is contained in:
16
README.md
16
README.md
@@ -350,6 +350,22 @@ as `gs` for the extended `git status`, and `ga` for the `git add` function.
|
|||||||
If you already have an alias like `alias gco="git checkout"`, you can now type
|
If you already have an alias like `alias gco="git checkout"`, you can now type
|
||||||
`gco 1` to checkout the first file in the output of SCM Breeze's `git status`.
|
`gco 1` to checkout the first file in the output of SCM Breeze's `git status`.
|
||||||
|
|
||||||
|
## Custom emojis for username and "staff" group
|
||||||
|
|
||||||
|
The `ll` command adds numbered shortcuts to files, but another fun feature is replacing your
|
||||||
|
username and the "staff" group with custom emojis. You can set these in `~/.user_sym` and `~/.staff_sym`.
|
||||||
|
|
||||||
|
<img src="/docs/images/custom_user_and_staff_symbols.jpg" width="400" alt="Custom user and staff emojis">
|
||||||
|
|
||||||
|
Set your own emojis by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo 🍀 > ~/.user_sym
|
||||||
|
echo 🖥 > ~/.staff_sym
|
||||||
|
```
|
||||||
|
|
||||||
|
I also like using `~/.user_sym` [in my Bash prompt](https://github.com/ndbroadbent/dotfiles/blob/master/bashrc/prompt.sh#L71).
|
||||||
|
|
||||||
|
|
||||||
## Notes about Tab Completion for Aliases
|
## Notes about Tab Completion for Aliases
|
||||||
|
|
||||||
|
|||||||
BIN
docs/images/custom_user_and_staff_symbols.jpg
Normal file
BIN
docs/images/custom_user_and_staff_symbols.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -53,6 +53,7 @@ git_diff_file_alias="gdf"
|
|||||||
git_diff_word_alias="gdw"
|
git_diff_word_alias="gdw"
|
||||||
git_diff_cached_alias="gdc"
|
git_diff_cached_alias="gdc"
|
||||||
git_difftool_alias="gdt"
|
git_difftool_alias="gdt"
|
||||||
|
git_mergetool_alias="gmt"
|
||||||
# 3. Standard commands
|
# 3. Standard commands
|
||||||
git_clone_alias="gcl"
|
git_clone_alias="gcl"
|
||||||
git_fetch_alias="gf"
|
git_fetch_alias="gf"
|
||||||
@@ -114,7 +115,8 @@ git_pull_request_alias="gpr"
|
|||||||
git_keyboard_shortcuts_enabled="true"
|
git_keyboard_shortcuts_enabled="true"
|
||||||
git_commit_all_keys="\C-x " # CTRL+x, SPACE
|
git_commit_all_keys="\C-x " # CTRL+x, SPACE
|
||||||
git_add_and_commit_keys="\C-xc" # CTRL+x, c
|
git_add_and_commit_keys="\C-xc" # CTRL+x, c
|
||||||
git_commit_all_with_ci_skip_keys="\C-xv" # CTRL+x, v (Appends [ci skip] to commit message)
|
git_commit_all_with_ci_skip_keys="\C-xv" # CTRL+x, v (Appends [ci skip] to message)
|
||||||
|
git_add_and_amend_commit_keys="\C-xz" # CTRL+x, z
|
||||||
|
|
||||||
|
|
||||||
# Shell Command Wrapping
|
# Shell Command Wrapping
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ if [ "$git_setup_aliases" = "yes" ]; then
|
|||||||
__git_alias "$git_add_patch_alias" 'git' 'add' '-p'
|
__git_alias "$git_add_patch_alias" 'git' 'add' '-p'
|
||||||
__git_alias "$git_add_updated_alias" 'git' 'add' '-u'
|
__git_alias "$git_add_updated_alias" 'git' 'add' '-u'
|
||||||
__git_alias "$git_difftool_alias" 'git' 'difftool'
|
__git_alias "$git_difftool_alias" 'git' 'difftool'
|
||||||
|
__git_alias "$git_mergetool_alias" 'git' 'mergetool'
|
||||||
|
|
||||||
# Custom default format for git log
|
# Custom default format for git log
|
||||||
git_log_command="log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
|
git_log_command="log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
|
||||||
|
|||||||
@@ -35,11 +35,13 @@ if [[ "$git_keyboard_shortcuts_enabled" = "true" ]]; then
|
|||||||
if [[ $shell == "zsh" ]]; then
|
if [[ $shell == "zsh" ]]; then
|
||||||
_bind "$git_commit_all_keys" " git_commit_all""$RETURN_CHAR"
|
_bind "$git_commit_all_keys" " git_commit_all""$RETURN_CHAR"
|
||||||
_bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""$RETURN_CHAR"
|
_bind "$git_add_and_commit_keys" " \033[1~ git_add_and_commit ""$RETURN_CHAR"
|
||||||
_bind "$git_commit_all_with_ci_skip_keys" " \033[1~ APPEND='[ci skip]' git_commit_all ""$RETURN_CHAR"
|
_bind "$git_commit_all_with_ci_skip_keys" " \033[1~ GIT_COMMIT_MSG_SUFFIX='[ci skip]' git_commit_all ""$RETURN_CHAR"
|
||||||
|
_bind "$git_add_and_amend_commit_keys" " git add --all . && git commit --amend -C HEAD""$RETURN_CHAR"
|
||||||
else
|
else
|
||||||
_bind "$git_commit_all_keys" "\" git_commit_all$RETURN_CHAR\""
|
_bind "$git_commit_all_keys" "\" git_commit_all$RETURN_CHAR\""
|
||||||
_bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit $RETURN_CHAR\""
|
_bind "$git_add_and_commit_keys" "\"\C-A git_add_and_commit $RETURN_CHAR\""
|
||||||
_bind "$git_commit_all_with_ci_skip_keys" "\"\C-A APPEND='[ci skip]' git_commit_all $RETURN_CHAR\""
|
_bind "$git_commit_all_with_ci_skip_keys" "\"\C-A GIT_COMMIT_MSG_SUFFIX='[ci skip]' git_commit_all $RETURN_CHAR\""
|
||||||
|
_bind "$git_add_and_amend_commit_keys" "\" git add --all . && git commit --amend -C HEAD$RETURN_CHAR\""
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -156,13 +156,24 @@ if [ "$shell_ls_aliases_enabled" = "true" ] && builtin command -v ruby > /dev/nu
|
|||||||
# After : -rw-rw-r-- 1 𝐍 𝐍 1.1K Sep 19 21:39 scm_breeze.sh
|
# After : -rw-rw-r-- 1 𝐍 𝐍 1.1K Sep 19 21:39 scm_breeze.sh
|
||||||
if [ -e "$HOME"/.user_sym ]; then
|
if [ -e "$HOME"/.user_sym ]; then
|
||||||
# Little bit of ruby golf to rejustify the user/group/size columns after replacement
|
# Little bit of ruby golf to rejustify the user/group/size columns after replacement
|
||||||
|
# TODO(ghthor): Convert this to a cat <<EOF to improve readibility
|
||||||
function rejustify_ls_columns(){
|
function rejustify_ls_columns(){
|
||||||
ruby -e "o=STDIN.read;re=/^(([^ ]* +){2})(([^ ]* +){3})/;\
|
ruby -e "o=STDIN.read;re=/^(([^ ]* +){2})(([^ ]* +){3})/;\
|
||||||
u,g,s=o.lines.map{|l|l[re,3]}.compact.map(&:split).transpose.map{|a|a.map(&:size).max+1};\
|
u,g,s=o.lines.map{|l|l[re,3]}.compact.map(&:split).transpose.map{|a|a.map(&:size).max+1};\
|
||||||
puts o.lines.map{|l|l.sub(re){|m|\"%s%-#{u}s %-#{g}s%#{s}s \"%[\$1,*\$3.split]}}"
|
puts o.lines.map{|l|l.sub(re){|m|\"%s%-#{u}s %-#{g}s%#{s}s \"%[\$1,*\$3.split]}}"
|
||||||
}
|
}
|
||||||
|
|
||||||
ll_output=$(echo "$ll_output" | \sed -$SED_REGEX_ARG "s/ $USER/ $(/bin/cat "$HOME/.user_sym")/g" | rejustify_ls_columns)
|
local USER_SYM=$(/bin/cat $HOME/.user_sym)
|
||||||
|
if [ -f "$HOME/.staff_sym" ]; then
|
||||||
|
local STAFF_SYM=$(/bin/cat $HOME/.staff_sym)
|
||||||
|
ll_output=$(echo "$ll_output" | \
|
||||||
|
\sed -$SED_REGEX_ARG "s/ $USER staff/ $USER_SYM $STAFF_SYM /g" | \
|
||||||
|
rejustify_ls_columns)
|
||||||
|
else
|
||||||
|
ll_output=$(echo "$ll_output" | \
|
||||||
|
\sed -$SED_REGEX_ARG "s/ $USER/ $USER_SYM /g" | \
|
||||||
|
rejustify_ls_columns)
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Bail if there are two many lines to process
|
# Bail if there are two many lines to process
|
||||||
|
|||||||
440
lib/git/status_shortcuts.rb
Executable file → Normal file
440
lib/git/status_shortcuts.rb
Executable file → Normal file
@@ -6,323 +6,213 @@
|
|||||||
# Released under the LGPL (GNU Lesser General Public License)
|
# Released under the LGPL (GNU Lesser General Public License)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Original work by Nathan Broadbent
|
|
||||||
# Rewritten by LFDM
|
|
||||||
#
|
|
||||||
# A much faster implementation of git_status_shortcuts() in ruby
|
# A much faster implementation of git_status_shortcuts() in ruby
|
||||||
# (original benchmarks - bash: 0m0.549s, ruby: 0m0.045s, the updated
|
# (bash: 0m0.549s, ruby: 0m0.045s)
|
||||||
# version is twice as fast, especially noticable in big repos)
|
|
||||||
#
|
|
||||||
#
|
#
|
||||||
# Last line of output contains the ordered absolute file paths,
|
# Last line of output contains the ordered absolute file paths,
|
||||||
# which need to be extracted by the shell and exported as numbered env variables.
|
# which need to be extracted by the shell and exported as numbered env variables.
|
||||||
#
|
#
|
||||||
# Processes 'git status', and exports numbered
|
# Processes 'git status --porcelain', and exports numbered
|
||||||
# env variables that contain the path of each affected file.
|
# env variables that contain the path of each affected file.
|
||||||
# Output is also more concise than standard 'git status'.
|
# Output is also more concise than standard 'git status'.
|
||||||
#
|
#
|
||||||
# Call with optional <group> parameter to just show one modification state
|
# Call with optional <group> parameter to just show one modification state
|
||||||
# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
|
# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
|
||||||
# --------------------------------------------------------------------
|
# --------------------------------------------------------------------
|
||||||
#
|
|
||||||
require 'strscan'
|
|
||||||
|
|
||||||
class GitStatus
|
@project_root = File.exist?(".git") ? Dir.pwd : `\git rev-parse --show-toplevel 2> /dev/null`.strip
|
||||||
def initialize(request = nil)
|
|
||||||
@request = request.to_s # capture nils
|
|
||||||
@status = get_status
|
|
||||||
@ahead, @behind = parse_remote_stat
|
|
||||||
@grouped_changes = parse_changes
|
|
||||||
@index = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def report
|
@git_status = `\git status --porcelain -b 2> /dev/null`
|
||||||
exit if all_changes.length > ENV["gs_max_changes"].to_i
|
|
||||||
print_header
|
git_status_lines = @git_status.split("\n")
|
||||||
print_groups
|
git_branch = git_status_lines[0]
|
||||||
puts filelist if @grouped_changes.any?
|
@branch = git_branch[/^## (?:Initial commit on )?([^ \.]+)/, 1]
|
||||||
end
|
@ahead = git_branch[/\[ahead ?(\d+).*\]/, 1]
|
||||||
|
@behind = git_branch[/\[.*behind ?(\d+)\]/, 1]
|
||||||
|
|
||||||
|
@changes = git_status_lines[1..-1]
|
||||||
|
# Exit if too many changes
|
||||||
|
exit if @changes.size > ENV["gs_max_changes"].to_i
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
@c = {
|
||||||
|
:rst => "\033[0m",
|
||||||
|
:del => "\033[0;31m",
|
||||||
|
:mod => "\033[0;32m",
|
||||||
|
:new => "\033[0;33m",
|
||||||
|
:ren => "\033[0;34m",
|
||||||
|
:cpy => "\033[0;33m",
|
||||||
|
:typ => "\033[0;35m",
|
||||||
|
:unt => "\033[0;36m",
|
||||||
|
:dark => "\033[2;37m",
|
||||||
|
:branch => "\033[1m",
|
||||||
|
:header => "\033[0m"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
######### Parsing methods #########
|
# Following colors must be prepended with modifiers e.g. '\033[1;', '\033[0;'
|
||||||
|
@group_c = {
|
||||||
|
:staged => "33m",
|
||||||
|
:unmerged => "31m",
|
||||||
|
:unstaged => "32m",
|
||||||
|
:untracked => "36m"
|
||||||
|
}
|
||||||
|
|
||||||
def get_status
|
@stat_hash = {
|
||||||
`git status 2>/dev/null`
|
:staged => [],
|
||||||
end
|
:unmerged => [],
|
||||||
|
:unstaged => [],
|
||||||
|
:untracked => []
|
||||||
|
}
|
||||||
|
|
||||||
# Remote info is always on the second line of git status
|
@output_files = []
|
||||||
def parse_remote_stat
|
|
||||||
remote_line = @status.lines[1].strip
|
|
||||||
if remote_line.match(/diverged/)
|
|
||||||
remote_line.match(/.*(\d*).*(\d*)/).captures
|
|
||||||
else
|
|
||||||
[remote_line[/is ahead of.*by (\d*).*/, 1], remote_line[/is behind.*by (\d*).*/, 1]]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# We have to resort to the StringScanner to stay away from overly complex
|
# Counter for env variables
|
||||||
# regular expressions.
|
@e = 0
|
||||||
# The individual blocks are always formatted the same
|
|
||||||
#
|
|
||||||
# identifier Changes not staged for commit
|
|
||||||
# helper text (use "git add <file>..." ...
|
|
||||||
# empty line
|
|
||||||
# changed files, leaded by a tab modified: file
|
|
||||||
# deleted: other_file
|
|
||||||
# empty line
|
|
||||||
# next identifier Untracked files
|
|
||||||
# ...
|
|
||||||
#
|
|
||||||
# We parse each requested group and return a grouped hash, its values are
|
|
||||||
# arrays of GitChange objects.
|
|
||||||
def parse_changes
|
|
||||||
scanner = StringScanner.new(@status)
|
|
||||||
requested_groups.each_with_object({}) do |(type, identifier), h|
|
|
||||||
if scanner.scan_until(/#{identifier}/)
|
|
||||||
scan_until_next_empty_line(scanner)
|
|
||||||
file_block = scan_until_next_empty_line(scanner)
|
|
||||||
h[type] = extract_changes(file_block)
|
|
||||||
end
|
|
||||||
scanner.reset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def scan_until_next_empty_line(scanner)
|
# Show how many commits we are ahead and/or behind origin
|
||||||
scanner.scan_until(/\n\n/)
|
difference = ["-#{@behind}", "+#{@ahead}"].select{|d| d.length > 1}.join('/')
|
||||||
end
|
difference = difference.length > 0 ? " #{@c[:dark]}| #{@c[:new]}#{difference}#{@c[:rst]}" : ""
|
||||||
|
|
||||||
# Matches
|
|
||||||
# modified: file # usual output in git status
|
|
||||||
# modified: file (untracked content) # output for submodules
|
|
||||||
# file # untracked files have no additional info
|
|
||||||
def extract_changes(str)
|
|
||||||
str.lines.map do |line|
|
|
||||||
new_git_change(*Regexp.last_match.captures) if line.match(/\t(.*:)?(.*)/)
|
|
||||||
end.compact # in case there were any non matching lines left
|
|
||||||
end
|
|
||||||
|
|
||||||
def new_git_change(status, file_and_message)
|
# If no changes, just display green no changes message and exit here
|
||||||
status = 'untracked:' unless status
|
if @git_status == ""
|
||||||
GitChange.new(file_and_message, status)
|
puts "%s#%s On branch: %s#{@branch}#{difference} %s| \033[0;32mNo changes (working directory clean)%s" % [
|
||||||
end
|
@c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst]
|
||||||
|
]
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
GROUPS = {
|
|
||||||
staged: 'Changes to be committed',
|
|
||||||
unmerged: 'Unmerged paths',
|
|
||||||
unstaged: 'Changes not staged for commit',
|
|
||||||
untracked: 'Untracked files'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Returns all possible groups when there was no request at all,
|
puts "%s#%s On branch: %s#{@branch}#{difference} %s| [%s*%s]%s => $#{ENV["git_env_char"]}*\n%s#%s" % [
|
||||||
# otherwise selects groups by name or integer
|
@c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst], @c[:dark], @c[:rst], @c[:dark], @c[:rst]
|
||||||
def requested_groups
|
]
|
||||||
@request.empty? ? GROUPS : select_groups
|
|
||||||
end
|
|
||||||
|
|
||||||
def select_groups
|
def has_modules?
|
||||||
req = parse_request
|
@has_modules ||= File.exists?(File.join(@project_root, '.gitmodules'))
|
||||||
GROUPS.select { |k, _| k == req }
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def parse_request
|
# Index modification states
|
||||||
if @request.match(/\d/)
|
@changes.each do |change|
|
||||||
GROUPS.keys[@request.to_i - 1]
|
x, y, file = change[0, 1], change[1, 1], change[3..-1]
|
||||||
else
|
|
||||||
@request.to_sym
|
# Fetch the long git status once, but only if any submodules have changed
|
||||||
|
if not @git_status_long and has_modules?
|
||||||
|
@gitmodules ||= File.read(File.join(@project_root, '.gitmodules'))
|
||||||
|
# If changed 'file' is actually a git submodule
|
||||||
|
if @gitmodules.include?(file)
|
||||||
|
# Parse long git status for submodule summaries
|
||||||
|
@git_status_long = `git status`.gsub(/\033\[[^m]*m/, "") # (strip colors)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
######### Outputting methods #########
|
msg, col, group = case change[0..1]
|
||||||
|
when "DD"; [" both deleted", :del, :unmerged]
|
||||||
def print_header
|
when "AU"; [" added by us", :new, :unmerged]
|
||||||
puts delimiter(:header) + header
|
when "UD"; ["deleted by them", :del, :unmerged]
|
||||||
puts delimiter(:header) if anything_changed?
|
when "UA"; [" added by them", :new, :unmerged]
|
||||||
|
when "DU"; [" deleted by us", :del, :unmerged]
|
||||||
|
when "AA"; [" both added", :new, :unmerged]
|
||||||
|
when "UU"; [" both modified", :mod, :unmerged]
|
||||||
|
when /M./; [" modified", :mod, :staged]
|
||||||
|
when /A./; [" new file", :new, :staged]
|
||||||
|
when /D./; [" deleted", :del, :staged]
|
||||||
|
when /R./; [" renamed", :ren, :staged]
|
||||||
|
when /C./; [" copied", :cpy, :staged]
|
||||||
|
when /T./; ["typechange", :typ, :staged]
|
||||||
|
when "??"; [" untracked", :unt, :untracked]
|
||||||
end
|
end
|
||||||
|
|
||||||
def print_groups
|
# Store data
|
||||||
@grouped_changes.each do |type, changes|
|
@stat_hash[group] << {:msg => msg, :col => col, :file => file} if msg
|
||||||
print_group_header(type)
|
|
||||||
puts delimiter(type)
|
|
||||||
changes.each do |change|
|
|
||||||
raise_index!
|
|
||||||
print delimiter(type)
|
|
||||||
puts change.report_with_index(@index, type, padding)
|
|
||||||
end
|
|
||||||
puts delimiter(type)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def print_group_header(type)
|
# Work tree modification states
|
||||||
puts "#{gmu('➤', type, 1)} #{GROUPS[type]}"
|
if x == "R" && y == "M"
|
||||||
end
|
# Extract the second file name from the format x -> y
|
||||||
|
quoted, unquoted = /^(?:"(?:[^"\\]|\\.)*"|[^"].*) -> (?:"((?:[^"\\]|\\.)*)"|(.*[^"]))$/.match(file)[1..2]
|
||||||
|
renamed_file = quoted || unquoted
|
||||||
######### Items of interest #########
|
@stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => renamed_file}
|
||||||
|
elsif x != "R" && y == "M"
|
||||||
def branch
|
@stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => file}
|
||||||
@status.lines.first.strip[/^On branch (.*)$/, 1]
|
elsif y == "D" && x != "D" && x != "U"
|
||||||
end
|
# Don't show deleted 'y' during a merge conflict.
|
||||||
|
@stat_hash[:unstaged] << {:msg => " deleted", :col => :del, :file => file}
|
||||||
def ahead
|
elsif y == "T"
|
||||||
"+#{@ahead}" if @ahead
|
@stat_hash[:unstaged] << {:msg => "typechange", :col => :typ, :file => file}
|
||||||
end
|
|
||||||
|
|
||||||
def behind
|
|
||||||
"-#{@behind}" if @behind
|
|
||||||
end
|
|
||||||
|
|
||||||
def difference
|
|
||||||
[behind, ahead].compact.join('/')
|
|
||||||
end
|
|
||||||
|
|
||||||
def header
|
|
||||||
parts = [[:branch, :branch], [:difference, :new]]
|
|
||||||
parts << (anything_changed? ? [:hotkey, :dark] : [:clean_state, :mod]) # mod is green
|
|
||||||
# compact because difference might return nil
|
|
||||||
"On branch: #{parts.map { |meth, col| mu(send(meth), col) }.compact.join(' | ')}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def clean_state
|
|
||||||
"No changes (working directory clean)"
|
|
||||||
end
|
|
||||||
|
|
||||||
def hotkey
|
|
||||||
"[*] => $#{ENV['git_env_char']}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# used to delimit the left side of the screen - looks nice
|
|
||||||
def delimiter(col)
|
|
||||||
gmu("# ", col)
|
|
||||||
end
|
|
||||||
|
|
||||||
def filelist
|
|
||||||
"@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}"
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
######### Helper Methods #########
|
|
||||||
|
|
||||||
# To know about changes we could ask if there are any parsing results, as in
|
|
||||||
# @grouped_changes.any?, but that is not a good idea, since
|
|
||||||
# we might have selected a requested group before parsing already.
|
|
||||||
# Groups could be empty while there are in fact changes present,
|
|
||||||
# there we look into the original status string once
|
|
||||||
def anything_changed?
|
|
||||||
@any_changes ||=
|
|
||||||
! @status.match(/nothing to commit.*working directory clean/)
|
|
||||||
end
|
|
||||||
|
|
||||||
# needed by hotkey filelist
|
|
||||||
def raise_index!
|
|
||||||
@index += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def all_changes
|
|
||||||
@all_changes ||= @grouped_changes.values.flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
# Dynamic padding, always looks for the longest status string present
|
|
||||||
# and adds a little whitespace
|
|
||||||
def padding
|
|
||||||
@padding ||= all_changes.map { |change| change.status.size }.max + 5
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
######### Markup/Color methods #########
|
|
||||||
|
|
||||||
COL = {
|
|
||||||
:rst => "0",
|
|
||||||
:header => "0",
|
|
||||||
:branch => "1",
|
|
||||||
:del => "0;31",
|
|
||||||
:mod => "0;32",
|
|
||||||
:new => "0;33",
|
|
||||||
:ren => "0;34",
|
|
||||||
:cpy => "0;33",
|
|
||||||
:typ => "0;35",
|
|
||||||
:unt => "0;36",
|
|
||||||
:dark => "2;37",
|
|
||||||
}
|
|
||||||
|
|
||||||
GR_COL = {
|
|
||||||
:staged => "33",
|
|
||||||
:unmerged => "31",
|
|
||||||
:unstaged => "32",
|
|
||||||
:untracked => "36",
|
|
||||||
}
|
|
||||||
|
|
||||||
# markup
|
|
||||||
def mu(str, col_in, col_out = :rst)
|
|
||||||
return if str.empty?
|
|
||||||
col_in = "\033[#{COL[col_in]}m"
|
|
||||||
col_out = "\033[#{COL[col_out]}m"
|
|
||||||
with_color(str, col_in, col_out)
|
|
||||||
end
|
|
||||||
|
|
||||||
# group markup
|
|
||||||
def gmu(str, group, boldness = 0, col_out = :rst)
|
|
||||||
group_col = "\033[#{boldness};#{GR_COL[group]}m"
|
|
||||||
col_out = "\033[#{COL[col_out]}m"
|
|
||||||
with_color(str, group_col, col_out)
|
|
||||||
end
|
|
||||||
|
|
||||||
def with_color(str, col_in, col_out)
|
|
||||||
"#{col_in}#{str}#{col_out}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class GitChange < GitStatus
|
def relative_path(base, target)
|
||||||
attr_reader :status
|
back = ""
|
||||||
|
while target.sub(base,'') == target
|
||||||
|
base = base.sub(/\/[^\/]*$/, '')
|
||||||
|
back = "../#{back}"
|
||||||
|
end
|
||||||
|
"#{back}#{target.sub("#{base}/",'')}"
|
||||||
|
end
|
||||||
|
|
||||||
# Restructively singles out the submodules message and
|
|
||||||
# strips the remaining string to get rid of padding
|
# Output files
|
||||||
def initialize(file_and_message, status)
|
def output_file_group(group)
|
||||||
@message = file_and_message.slice!(/\(.*\)/)
|
# Print colored hashes & files based on modification groups
|
||||||
@file = file_and_message.strip
|
c_group = "\033[0;#{@group_c[group]}"
|
||||||
@file = (@file.include? " ") ? "\"#{@file}\"" : @file
|
|
||||||
@status = status.strip
|
@stat_hash[group].each do |h|
|
||||||
|
@e += 1
|
||||||
|
padding = (@e < 10 && @changes.size >= 10) ? " " : ""
|
||||||
|
|
||||||
|
# Find relative path, i.e. ../../lib/path/to/file
|
||||||
|
rel_file = relative_path(Dir.pwd, File.join(@project_root, h[:file]))
|
||||||
|
|
||||||
|
# If some submodules have changed, parse their summaries from long git status
|
||||||
|
sub_stat = nil
|
||||||
|
if @git_status_long && (sub_stat = @git_status_long[/#{h[:file]} \((.*)\)/, 1])
|
||||||
|
# Format summary with parantheses
|
||||||
|
sub_stat = "(#{sub_stat})"
|
||||||
end
|
end
|
||||||
|
|
||||||
def absolute_path
|
puts "#{c_group}##{@c[:rst]} #{@c[h[:col]]}#{h[:msg]}:\
|
||||||
File.expand_path(@file, Dir.pwd)
|
#{padding}#{@c[:dark]} [#{@c[:rst]}#{@e}#{@c[:dark]}] #{c_group}#{rel_file}#{@c[:rst]} #{sub_stat}"
|
||||||
|
# Save the ordered list of output files
|
||||||
|
# fetch first file (in the case of oldFile -> newFile) and remove quotes
|
||||||
|
@output_files << if h[:msg] == "typechange"
|
||||||
|
# Only use relative paths for 'typechange' modifications.
|
||||||
|
"~#{rel_file}"
|
||||||
|
elsif h[:file] =~ /^"([^\\"]*(\\.[^"]*)*)"/
|
||||||
|
# Handle the regex above..
|
||||||
|
$1.gsub(/\\(.)/,'\1')
|
||||||
|
else
|
||||||
|
# Else, strip file
|
||||||
|
h[:file].strip
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
STATUS_COLORS = {
|
puts "#{c_group}##{@c[:rst]}" # Extra '#'
|
||||||
"copied" => :cpy,
|
end
|
||||||
"both deleted" => :del,
|
|
||||||
"deleted by us" => :del,
|
|
||||||
"deleted by them" => :del,
|
|
||||||
"deleted" => :del,
|
|
||||||
"both modified" => :mod,
|
|
||||||
"modified" => :mod,
|
|
||||||
"added by them" => :new,
|
|
||||||
"added by us" => :new,
|
|
||||||
"both added" => :new,
|
|
||||||
"new file" => :new,
|
|
||||||
"renamed" => :ren,
|
|
||||||
"typechange" => :typ,
|
|
||||||
"untracked" => :unt,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Looks like this
|
|
||||||
#
|
[[:staged, 'Changes to be committed'],
|
||||||
# PADDING STATUS INDEX FILE MESSAGE (optional)
|
[:unmerged, 'Unmerged paths'],
|
||||||
# modified: [1] changed_file (untracked content)
|
[:unstaged, 'Changes not staged for commit'],
|
||||||
#
|
[:untracked, 'Untracked files']
|
||||||
def report_with_index(index, type, padding = 0)
|
].each_with_index do |data, i|
|
||||||
"#{pad(padding)}#{mu(@status, color_key)} " +
|
group, heading = *data
|
||||||
"#{mu("[#{index}]", :dark)} #{gmu(@file, type)} #{@message}"
|
|
||||||
|
# Allow filtering by specific group (by string or integer)
|
||||||
|
if !ARGV[0] || ARGV[0] == group.to_s || ARGV[0] == (i+1).to_s; then
|
||||||
|
if !@stat_hash[group].empty?
|
||||||
|
c_arrow="\033[1;#{@group_c[group]}"
|
||||||
|
c_hash="\033[0;#{@group_c[group]}"
|
||||||
|
puts "#{c_arrow}➤#{@c[:header]} #{heading}\n#{c_hash}##{@c[:rst]}"
|
||||||
|
output_file_group(group)
|
||||||
end
|
end
|
||||||
|
|
||||||
# we most likely have a : with us which we don't need here
|
|
||||||
def color_key
|
|
||||||
STATUS_COLORS[@status.chomp(':')]
|
|
||||||
end
|
|
||||||
|
|
||||||
def pad(padding)
|
|
||||||
' ' * (padding - @status.size)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
GitStatus.new(ARGV.first).report
|
print "@@filelist@@::"
|
||||||
|
puts @output_files.map {|f|
|
||||||
|
# If file starts with a '~', treat it as a relative path.
|
||||||
|
# This is important when dealing with symlinks
|
||||||
|
f.start_with?("~") ? f.sub(/~/, '') : File.join(@project_root, f)
|
||||||
|
}.join("|")
|
||||||
|
|||||||
@@ -1,218 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
# encoding: UTF-8
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# SCM Breeze - Streamline your SCM workflow.
|
|
||||||
# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
|
|
||||||
# Released under the LGPL (GNU Lesser General Public License)
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
#
|
|
||||||
# A much faster implementation of git_status_shortcuts() in ruby
|
|
||||||
# (bash: 0m0.549s, ruby: 0m0.045s)
|
|
||||||
#
|
|
||||||
# Last line of output contains the ordered absolute file paths,
|
|
||||||
# which need to be extracted by the shell and exported as numbered env variables.
|
|
||||||
#
|
|
||||||
# Processes 'git status --porcelain', and exports numbered
|
|
||||||
# env variables that contain the path of each affected file.
|
|
||||||
# Output is also more concise than standard 'git status'.
|
|
||||||
#
|
|
||||||
# Call with optional <group> parameter to just show one modification state
|
|
||||||
# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
|
|
||||||
# --------------------------------------------------------------------
|
|
||||||
|
|
||||||
@project_root = File.exist?(".git") ? Dir.pwd : `\git rev-parse --show-toplevel 2> /dev/null`.strip
|
|
||||||
|
|
||||||
@git_status = `\git status --porcelain -b 2> /dev/null`
|
|
||||||
|
|
||||||
git_status_lines = @git_status.split("\n")
|
|
||||||
git_branch = git_status_lines[0]
|
|
||||||
@branch = git_branch[/^## (?:Initial commit on )?([^ \.]+)/, 1]
|
|
||||||
@ahead = git_branch[/\[ahead ?(\d+).*\]/, 1]
|
|
||||||
@behind = git_branch[/\[.*behind ?(\d+)\]/, 1]
|
|
||||||
|
|
||||||
@changes = git_status_lines[1..-1]
|
|
||||||
# Exit if too many changes
|
|
||||||
exit if @changes.size > ENV["gs_max_changes"].to_i
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
@c = {
|
|
||||||
:rst => "\033[0m",
|
|
||||||
:del => "\033[0;31m",
|
|
||||||
:mod => "\033[0;32m",
|
|
||||||
:new => "\033[0;33m",
|
|
||||||
:ren => "\033[0;34m",
|
|
||||||
:cpy => "\033[0;33m",
|
|
||||||
:typ => "\033[0;35m",
|
|
||||||
:unt => "\033[0;36m",
|
|
||||||
:dark => "\033[2;37m",
|
|
||||||
:branch => "\033[1m",
|
|
||||||
:header => "\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Following colors must be prepended with modifiers e.g. '\033[1;', '\033[0;'
|
|
||||||
@group_c = {
|
|
||||||
:staged => "33m",
|
|
||||||
:unmerged => "31m",
|
|
||||||
:unstaged => "32m",
|
|
||||||
:untracked => "36m"
|
|
||||||
}
|
|
||||||
|
|
||||||
@stat_hash = {
|
|
||||||
:staged => [],
|
|
||||||
:unmerged => [],
|
|
||||||
:unstaged => [],
|
|
||||||
:untracked => []
|
|
||||||
}
|
|
||||||
|
|
||||||
@output_files = []
|
|
||||||
|
|
||||||
# Counter for env variables
|
|
||||||
@e = 0
|
|
||||||
|
|
||||||
# Show how many commits we are ahead and/or behind origin
|
|
||||||
difference = ["-#{@behind}", "+#{@ahead}"].select{|d| d.length > 1}.join('/')
|
|
||||||
difference = difference.length > 0 ? " #{@c[:dark]}| #{@c[:new]}#{difference}#{@c[:rst]}" : ""
|
|
||||||
|
|
||||||
|
|
||||||
# If no changes, just display green no changes message and exit here
|
|
||||||
if @git_status == ""
|
|
||||||
puts "%s#%s On branch: %s#{@branch}#{difference} %s| \033[0;32mNo changes (working directory clean)%s" % [
|
|
||||||
@c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst]
|
|
||||||
]
|
|
||||||
exit
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
puts "%s#%s On branch: %s#{@branch}#{difference} %s| [%s*%s]%s => $#{ENV["git_env_char"]}*\n%s#%s" % [
|
|
||||||
@c[:dark], @c[:rst], @c[:branch], @c[:dark], @c[:rst], @c[:dark], @c[:rst], @c[:dark], @c[:rst]
|
|
||||||
]
|
|
||||||
|
|
||||||
def has_modules?
|
|
||||||
@has_modules ||= File.exists?(File.join(@project_root, '.gitmodules'))
|
|
||||||
end
|
|
||||||
|
|
||||||
# Index modification states
|
|
||||||
@changes.each do |change|
|
|
||||||
x, y, file = change[0, 1], change[1, 1], change[3..-1]
|
|
||||||
|
|
||||||
# Fetch the long git status once, but only if any submodules have changed
|
|
||||||
if not @git_status_long and has_modules?
|
|
||||||
@gitmodules ||= File.read(File.join(@project_root, '.gitmodules'))
|
|
||||||
# If changed 'file' is actually a git submodule
|
|
||||||
if @gitmodules.include?(file)
|
|
||||||
# Parse long git status for submodule summaries
|
|
||||||
@git_status_long = `git status`.gsub(/\033\[[^m]*m/, "") # (strip colors)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
msg, col, group = case change[0..1]
|
|
||||||
when "DD"; [" both deleted", :del, :unmerged]
|
|
||||||
when "AU"; [" added by us", :new, :unmerged]
|
|
||||||
when "UD"; ["deleted by them", :del, :unmerged]
|
|
||||||
when "UA"; [" added by them", :new, :unmerged]
|
|
||||||
when "DU"; [" deleted by us", :del, :unmerged]
|
|
||||||
when "AA"; [" both added", :new, :unmerged]
|
|
||||||
when "UU"; [" both modified", :mod, :unmerged]
|
|
||||||
when /M./; [" modified", :mod, :staged]
|
|
||||||
when /A./; [" new file", :new, :staged]
|
|
||||||
when /D./; [" deleted", :del, :staged]
|
|
||||||
when /R./; [" renamed", :ren, :staged]
|
|
||||||
when /C./; [" copied", :cpy, :staged]
|
|
||||||
when /T./; ["typechange", :typ, :staged]
|
|
||||||
when "??"; [" untracked", :unt, :untracked]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Store data
|
|
||||||
@stat_hash[group] << {:msg => msg, :col => col, :file => file} if msg
|
|
||||||
|
|
||||||
# Work tree modification states
|
|
||||||
if x == "R" && y == "M"
|
|
||||||
# Extract the second file name from the format x -> y
|
|
||||||
quoted, unquoted = /^(?:"(?:[^"\\]|\\.)*"|[^"].*) -> (?:"((?:[^"\\]|\\.)*)"|(.*[^"]))$/.match(file)[1..2]
|
|
||||||
renamed_file = quoted || unquoted
|
|
||||||
@stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => renamed_file}
|
|
||||||
elsif x != "R" && y == "M"
|
|
||||||
@stat_hash[:unstaged] << {:msg => " modified", :col => :mod, :file => file}
|
|
||||||
elsif y == "D" && x != "D" && x != "U"
|
|
||||||
# Don't show deleted 'y' during a merge conflict.
|
|
||||||
@stat_hash[:unstaged] << {:msg => " deleted", :col => :del, :file => file}
|
|
||||||
elsif y == "T"
|
|
||||||
@stat_hash[:unstaged] << {:msg => "typechange", :col => :typ, :file => file}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def relative_path(base, target)
|
|
||||||
back = ""
|
|
||||||
while target.sub(base,'') == target
|
|
||||||
base = base.sub(/\/[^\/]*$/, '')
|
|
||||||
back = "../#{back}"
|
|
||||||
end
|
|
||||||
"#{back}#{target.sub("#{base}/",'')}"
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Output files
|
|
||||||
def output_file_group(group)
|
|
||||||
# Print colored hashes & files based on modification groups
|
|
||||||
c_group = "\033[0;#{@group_c[group]}"
|
|
||||||
|
|
||||||
@stat_hash[group].each do |h|
|
|
||||||
@e += 1
|
|
||||||
padding = (@e < 10 && @changes.size >= 10) ? " " : ""
|
|
||||||
|
|
||||||
# Find relative path, i.e. ../../lib/path/to/file
|
|
||||||
rel_file = relative_path(Dir.pwd, File.join(@project_root, h[:file]))
|
|
||||||
|
|
||||||
# If some submodules have changed, parse their summaries from long git status
|
|
||||||
sub_stat = nil
|
|
||||||
if @git_status_long && (sub_stat = @git_status_long[/#{h[:file]} \((.*)\)/, 1])
|
|
||||||
# Format summary with parantheses
|
|
||||||
sub_stat = "(#{sub_stat})"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{c_group}##{@c[:rst]} #{@c[h[:col]]}#{h[:msg]}:\
|
|
||||||
#{padding}#{@c[:dark]} [#{@c[:rst]}#{@e}#{@c[:dark]}] #{c_group}#{rel_file}#{@c[:rst]} #{sub_stat}"
|
|
||||||
# Save the ordered list of output files
|
|
||||||
# fetch first file (in the case of oldFile -> newFile) and remove quotes
|
|
||||||
@output_files << if h[:msg] == "typechange"
|
|
||||||
# Only use relative paths for 'typechange' modifications.
|
|
||||||
"~#{rel_file}"
|
|
||||||
elsif h[:file] =~ /^"([^\\"]*(\\.[^"]*)*)"/
|
|
||||||
# Handle the regex above..
|
|
||||||
$1.gsub(/\\(.)/,'\1')
|
|
||||||
else
|
|
||||||
# Else, strip file
|
|
||||||
h[:file].strip
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "#{c_group}##{@c[:rst]}" # Extra '#'
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
[[:staged, 'Changes to be committed'],
|
|
||||||
[:unmerged, 'Unmerged paths'],
|
|
||||||
[:unstaged, 'Changes not staged for commit'],
|
|
||||||
[:untracked, 'Untracked files']
|
|
||||||
].each_with_index do |data, i|
|
|
||||||
group, heading = *data
|
|
||||||
|
|
||||||
# Allow filtering by specific group (by string or integer)
|
|
||||||
if !ARGV[0] || ARGV[0] == group.to_s || ARGV[0] == (i+1).to_s; then
|
|
||||||
if !@stat_hash[group].empty?
|
|
||||||
c_arrow="\033[1;#{@group_c[group]}"
|
|
||||||
c_hash="\033[0;#{@group_c[group]}"
|
|
||||||
puts "#{c_arrow}➤#{@c[:header]} #{heading}\n#{c_hash}##{@c[:rst]}"
|
|
||||||
output_file_group(group)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
print "@@filelist@@::"
|
|
||||||
puts @output_files.map {|f|
|
|
||||||
# If file starts with a '~', treat it as a relative path.
|
|
||||||
# This is important when dealing with symlinks
|
|
||||||
f.start_with?("~") ? f.sub(/~/, '') : File.join(@project_root, f)
|
|
||||||
}.join("|")
|
|
||||||
@@ -211,29 +211,62 @@ theirs(){ _git_resolve_merge_conflict "their" "$@"; }
|
|||||||
# * Add escaped commit command and unescaped message to bash history.
|
# * Add escaped commit command and unescaped message to bash history.
|
||||||
git_commit_prompt() {
|
git_commit_prompt() {
|
||||||
local commit_msg
|
local commit_msg
|
||||||
|
local saved_commit_msg
|
||||||
|
if [ -f "/tmp/.git_commit_message~" ]; then
|
||||||
|
saved_commit_msg="$(cat /tmp/.git_commit_message~)"
|
||||||
|
echo -e "\033[0;36mLeave blank to use saved commit message: \033[0m$saved_commit_msg"
|
||||||
|
fi
|
||||||
if [[ $shell == "zsh" ]]; then
|
if [[ $shell == "zsh" ]]; then
|
||||||
vared -h -p "Commit Message: " commit_msg
|
vared -h -p "Commit Message: " commit_msg
|
||||||
else
|
else
|
||||||
read -r -e -p "Commit Message: " commit_msg
|
read -r -e -p "Commit Message: " commit_msg
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$commit_msg" ]; then
|
if [ -z "$commit_msg" ]; then
|
||||||
eval "$@" # run any prequisite commands
|
if [ -n "$saved_commit_msg" ]; then
|
||||||
# Add $APPEND to commit message, if given. (Used to append things like [ci skip] for Travis CI)
|
commit_msg="$saved_commit_msg"
|
||||||
if [ -n "$APPEND" ]; then commit_msg="$commit_msg $APPEND"; fi
|
|
||||||
echo $commit_msg | git commit -F - | tail -n +2
|
|
||||||
else
|
else
|
||||||
echo -e "\033[0;31mAborting commit due to empty commit message.\033[0m"
|
echo -e "\033[0;31mAborting commit due to empty commit message.\033[0m"
|
||||||
|
return
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
escaped=$(echo "$commit_msg" | sed -e 's/"/\\"/g' -e 's/!/"'"'"'!'"'"'"/g')
|
|
||||||
|
|
||||||
|
# Add $GIT_COMMIT_MSG_SUFFIX to commit message, if given.
|
||||||
|
# (Used to append things like [ci skip] for Travis CI)
|
||||||
|
if [ -n "$GIT_COMMIT_MSG_SUFFIX" ]; then
|
||||||
|
commit_msg="$commit_msg $GIT_COMMIT_MSG_SUFFIX"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exclamation marks are really difficult to escape properly in a bash prompt.
|
||||||
|
# They must always be enclosed with single quotes.
|
||||||
|
escaped_msg=$(echo "$commit_msg" | sed -e 's/"/\\"/g' -e "s/!/\"'!'\"/g")
|
||||||
|
# Add command to bash history, so that if a git pre-commit hook fails,
|
||||||
|
# you can just press "up" and "return" to retry the commit.
|
||||||
if [[ $shell == "zsh" ]]; then
|
if [[ $shell == "zsh" ]]; then
|
||||||
print -s "git commit -m \"${escaped//\\/\\\\}\"" # zsh's print needs double escaping
|
# zsh's print needs double escaping
|
||||||
print -s "$commit_msg"
|
print -s "git commit -m \"${escaped_msg//\\/\\\\}\""
|
||||||
else
|
else
|
||||||
echo "git commit -m \"$escaped\"" >> $HISTFILE
|
history -s "git commit -m \"$escaped_msg\""
|
||||||
# Also add unescaped commit message, for git prompt
|
# Need to write history to a file for tests
|
||||||
echo "$commit_msg" >> $HISTFILE
|
if [ -n "$SHUNIT_VERSION" ]; then history -w $HISTFILE; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also save the commit message to a temp file in case git commit fails
|
||||||
|
echo "$commit_msg" > "/tmp/.git_commit_message~"
|
||||||
|
eval $@ # run any prequisite commands
|
||||||
|
|
||||||
|
echo "$commit_msg" | git commit -F - | tail -n +2
|
||||||
|
|
||||||
|
# Fetch the pipe status (for both bash and zsh):
|
||||||
|
GIT_PIPE_STATUS=("${PIPESTATUS[@]}${pipestatus[@]}")
|
||||||
|
if [[ $shell == "zsh" ]]; then
|
||||||
|
git_exit_status="${GIT_PIPE_STATUS[2]}" # zsh array indexes start at 1
|
||||||
|
else
|
||||||
|
git_exit_status="${GIT_PIPE_STATUS[1]}"
|
||||||
|
fi
|
||||||
|
if [[ "$git_exit_status" == 0 ]]; then
|
||||||
|
# Delete saved commit message if commit was successful
|
||||||
|
rm -f "/tmp/.git_commit_message~"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,8 +275,8 @@ git_commit_all() {
|
|||||||
fail_if_not_git_repo || return 1
|
fail_if_not_git_repo || return 1
|
||||||
changes=$(git status --porcelain | wc -l | tr -d ' ')
|
changes=$(git status --porcelain | wc -l | tr -d ' ')
|
||||||
if [ "$changes" -gt 0 ]; then
|
if [ "$changes" -gt 0 ]; then
|
||||||
if [ -n "$APPEND" ]; then
|
if [ -n "$GIT_COMMIT_MSG_SUFFIX" ]; then
|
||||||
local appending=" | \033[0;36mappending '\033[1;36m$APPEND\033[0;36m' to commit message.\033[0m"
|
local appending=" | \033[0;36mappending '\033[1;36m$GIT_COMMIT_MSG_SUFFIX\033[0;36m' to commit message.\033[0m"
|
||||||
fi
|
fi
|
||||||
echo -e "\033[0;33mCommitting all files (\033[0;31m$changes\033[0;33m)\033[0m$appending"
|
echo -e "\033[0;33mCommitting all files (\033[0;31m$changes\033[0;33m)\033[0m$appending"
|
||||||
git_commit_prompt "git add --all ."
|
git_commit_prompt "git add --all ."
|
||||||
|
|||||||
328
lib/git/status_shortcuts_refactor.rb
Normal file
328
lib/git/status_shortcuts_refactor.rb
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# encoding: UTF-8
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# SCM Breeze - Streamline your SCM workflow.
|
||||||
|
# Copyright 2011 Nathan Broadbent (http://madebynathan.com). All Rights Reserved.
|
||||||
|
# Released under the LGPL (GNU Lesser General Public License)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Original work by Nathan Broadbent
|
||||||
|
# Rewritten by LFDM
|
||||||
|
#
|
||||||
|
# A much faster implementation of git_status_shortcuts() in ruby
|
||||||
|
# (original benchmarks - bash: 0m0.549s, ruby: 0m0.045s, the updated
|
||||||
|
# version is twice as fast, especially noticable in big repos)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Last line of output contains the ordered absolute file paths,
|
||||||
|
# which need to be extracted by the shell and exported as numbered env variables.
|
||||||
|
#
|
||||||
|
# Processes 'git status', and exports numbered
|
||||||
|
# env variables that contain the path of each affected file.
|
||||||
|
# Output is also more concise than standard 'git status'.
|
||||||
|
#
|
||||||
|
# Call with optional <group> parameter to just show one modification state
|
||||||
|
# # groups => 1: staged, 2: unmerged, 3: unstaged, 4: untracked
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
require 'strscan'
|
||||||
|
|
||||||
|
class GitStatus
|
||||||
|
def initialize(request = nil)
|
||||||
|
@request = request.to_s # capture nils
|
||||||
|
@status = get_status
|
||||||
|
@ahead, @behind = parse_remote_stat
|
||||||
|
@grouped_changes = parse_changes
|
||||||
|
@index = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def report
|
||||||
|
exit if all_changes.length > ENV["gs_max_changes"].to_i
|
||||||
|
print_header
|
||||||
|
print_groups
|
||||||
|
puts filelist if @grouped_changes.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
######### Parsing methods #########
|
||||||
|
|
||||||
|
def get_status
|
||||||
|
`git status 2>/dev/null`
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remote info is always on the second line of git status
|
||||||
|
def parse_remote_stat
|
||||||
|
remote_line = @status.lines[1].strip
|
||||||
|
if remote_line.match(/diverged/)
|
||||||
|
remote_line.match(/.*(\d*).*(\d*)/).captures
|
||||||
|
else
|
||||||
|
[remote_line[/is ahead of.*by (\d*).*/, 1], remote_line[/is behind.*by (\d*).*/, 1]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# We have to resort to the StringScanner to stay away from overly complex
|
||||||
|
# regular expressions.
|
||||||
|
# The individual blocks are always formatted the same
|
||||||
|
#
|
||||||
|
# identifier Changes not staged for commit
|
||||||
|
# helper text (use "git add <file>..." ...
|
||||||
|
# empty line
|
||||||
|
# changed files, leaded by a tab modified: file
|
||||||
|
# deleted: other_file
|
||||||
|
# empty line
|
||||||
|
# next identifier Untracked files
|
||||||
|
# ...
|
||||||
|
#
|
||||||
|
# We parse each requested group and return a grouped hash, its values are
|
||||||
|
# arrays of GitChange objects.
|
||||||
|
def parse_changes
|
||||||
|
scanner = StringScanner.new(@status)
|
||||||
|
requested_groups.each_with_object({}) do |(type, identifier), h|
|
||||||
|
if scanner.scan_until(/#{identifier}/)
|
||||||
|
scan_until_next_empty_line(scanner)
|
||||||
|
file_block = scan_until_next_empty_line(scanner)
|
||||||
|
h[type] = extract_changes(file_block)
|
||||||
|
end
|
||||||
|
scanner.reset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def scan_until_next_empty_line(scanner)
|
||||||
|
scanner.scan_until(/\n\n/)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Matches
|
||||||
|
# modified: file # usual output in git status
|
||||||
|
# modified: file (untracked content) # output for submodules
|
||||||
|
# file # untracked files have no additional info
|
||||||
|
def extract_changes(str)
|
||||||
|
str.lines.map do |line|
|
||||||
|
new_git_change(*Regexp.last_match.captures) if line.match(/\t(.*:)?(.*)/)
|
||||||
|
end.compact # in case there were any non matching lines left
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_git_change(status, file_and_message)
|
||||||
|
status = 'untracked:' unless status
|
||||||
|
GitChange.new(file_and_message, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
GROUPS = {
|
||||||
|
staged: 'Changes to be committed',
|
||||||
|
unmerged: 'Unmerged paths',
|
||||||
|
unstaged: 'Changes not staged for commit',
|
||||||
|
untracked: 'Untracked files'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns all possible groups when there was no request at all,
|
||||||
|
# otherwise selects groups by name or integer
|
||||||
|
def requested_groups
|
||||||
|
@request.empty? ? GROUPS : select_groups
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_groups
|
||||||
|
req = parse_request
|
||||||
|
GROUPS.select { |k, _| k == req }
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_request
|
||||||
|
if @request.match(/\d/)
|
||||||
|
GROUPS.keys[@request.to_i - 1]
|
||||||
|
else
|
||||||
|
@request.to_sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
######### Outputting methods #########
|
||||||
|
|
||||||
|
def print_header
|
||||||
|
puts delimiter(:header) + header
|
||||||
|
puts delimiter(:header) if anything_changed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def print_groups
|
||||||
|
@grouped_changes.each do |type, changes|
|
||||||
|
print_group_header(type)
|
||||||
|
puts delimiter(type)
|
||||||
|
changes.each do |change|
|
||||||
|
raise_index!
|
||||||
|
print delimiter(type)
|
||||||
|
puts change.report_with_index(@index, type, padding)
|
||||||
|
end
|
||||||
|
puts delimiter(type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def print_group_header(type)
|
||||||
|
puts "#{gmu('➤', type, 1)} #{GROUPS[type]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
######### Items of interest #########
|
||||||
|
|
||||||
|
def branch
|
||||||
|
@status.lines.first.strip[/^On branch (.*)$/, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def ahead
|
||||||
|
"+#{@ahead}" if @ahead
|
||||||
|
end
|
||||||
|
|
||||||
|
def behind
|
||||||
|
"-#{@behind}" if @behind
|
||||||
|
end
|
||||||
|
|
||||||
|
def difference
|
||||||
|
[behind, ahead].compact.join('/')
|
||||||
|
end
|
||||||
|
|
||||||
|
def header
|
||||||
|
parts = [[:branch, :branch], [:difference, :new]]
|
||||||
|
parts << (anything_changed? ? [:hotkey, :dark] : [:clean_state, :mod]) # mod is green
|
||||||
|
# compact because difference might return nil
|
||||||
|
"On branch: #{parts.map { |meth, col| mu(send(meth), col) }.compact.join(' | ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def clean_state
|
||||||
|
"No changes (working directory clean)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def hotkey
|
||||||
|
"[*] => $#{ENV['git_env_char']}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# used to delimit the left side of the screen - looks nice
|
||||||
|
def delimiter(col)
|
||||||
|
gmu("# ", col)
|
||||||
|
end
|
||||||
|
|
||||||
|
def filelist
|
||||||
|
"@@filelist@@::#{all_changes.map(&:absolute_path).join('|')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
######### Helper Methods #########
|
||||||
|
|
||||||
|
# To know about changes we could ask if there are any parsing results, as in
|
||||||
|
# @grouped_changes.any?, but that is not a good idea, since
|
||||||
|
# we might have selected a requested group before parsing already.
|
||||||
|
# Groups could be empty while there are in fact changes present,
|
||||||
|
# there we look into the original status string once
|
||||||
|
def anything_changed?
|
||||||
|
@any_changes ||=
|
||||||
|
! @status.match(/nothing to commit.*working directory clean/)
|
||||||
|
end
|
||||||
|
|
||||||
|
# needed by hotkey filelist
|
||||||
|
def raise_index!
|
||||||
|
@index += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_changes
|
||||||
|
@all_changes ||= @grouped_changes.values.flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
# Dynamic padding, always looks for the longest status string present
|
||||||
|
# and adds a little whitespace
|
||||||
|
def padding
|
||||||
|
@padding ||= all_changes.map { |change| change.status.size }.max + 5
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
######### Markup/Color methods #########
|
||||||
|
|
||||||
|
COL = {
|
||||||
|
:rst => "0",
|
||||||
|
:header => "0",
|
||||||
|
:branch => "1",
|
||||||
|
:del => "0;31",
|
||||||
|
:mod => "0;32",
|
||||||
|
:new => "0;33",
|
||||||
|
:ren => "0;34",
|
||||||
|
:cpy => "0;33",
|
||||||
|
:typ => "0;35",
|
||||||
|
:unt => "0;36",
|
||||||
|
:dark => "2;37",
|
||||||
|
}
|
||||||
|
|
||||||
|
GR_COL = {
|
||||||
|
:staged => "33",
|
||||||
|
:unmerged => "31",
|
||||||
|
:unstaged => "32",
|
||||||
|
:untracked => "36",
|
||||||
|
}
|
||||||
|
|
||||||
|
# markup
|
||||||
|
def mu(str, col_in, col_out = :rst)
|
||||||
|
return if str.empty?
|
||||||
|
col_in = "\033[#{COL[col_in]}m"
|
||||||
|
col_out = "\033[#{COL[col_out]}m"
|
||||||
|
with_color(str, col_in, col_out)
|
||||||
|
end
|
||||||
|
|
||||||
|
# group markup
|
||||||
|
def gmu(str, group, boldness = 0, col_out = :rst)
|
||||||
|
group_col = "\033[#{boldness};#{GR_COL[group]}m"
|
||||||
|
col_out = "\033[#{COL[col_out]}m"
|
||||||
|
with_color(str, group_col, col_out)
|
||||||
|
end
|
||||||
|
|
||||||
|
def with_color(str, col_in, col_out)
|
||||||
|
"#{col_in}#{str}#{col_out}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class GitChange < GitStatus
|
||||||
|
attr_reader :status
|
||||||
|
|
||||||
|
# Restructively singles out the submodules message and
|
||||||
|
# strips the remaining string to get rid of padding
|
||||||
|
def initialize(file_and_message, status)
|
||||||
|
@message = file_and_message.slice!(/\(.*\)/)
|
||||||
|
@file = file_and_message.strip
|
||||||
|
@file = (@file.include? " ") ? "\"#{@file}\"" : @file
|
||||||
|
@status = status.strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def absolute_path
|
||||||
|
File.expand_path(@file, Dir.pwd)
|
||||||
|
end
|
||||||
|
|
||||||
|
STATUS_COLORS = {
|
||||||
|
"copied" => :cpy,
|
||||||
|
"both deleted" => :del,
|
||||||
|
"deleted by us" => :del,
|
||||||
|
"deleted by them" => :del,
|
||||||
|
"deleted" => :del,
|
||||||
|
"both modified" => :mod,
|
||||||
|
"modified" => :mod,
|
||||||
|
"added by them" => :new,
|
||||||
|
"added by us" => :new,
|
||||||
|
"both added" => :new,
|
||||||
|
"new file" => :new,
|
||||||
|
"renamed" => :ren,
|
||||||
|
"typechange" => :typ,
|
||||||
|
"untracked" => :unt,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Looks like this
|
||||||
|
#
|
||||||
|
# PADDING STATUS INDEX FILE MESSAGE (optional)
|
||||||
|
# modified: [1] changed_file (untracked content)
|
||||||
|
#
|
||||||
|
def report_with_index(index, type, padding = 0)
|
||||||
|
"#{pad(padding)}#{mu(@status, color_key)} " +
|
||||||
|
"#{mu("[#{index}]", :dark)} #{gmu(@file, type)} #{@message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# we most likely have a : with us which we don't need here
|
||||||
|
def color_key
|
||||||
|
STATUS_COLORS[@status.chomp(':')]
|
||||||
|
end
|
||||||
|
|
||||||
|
def pad(padding)
|
||||||
|
' ' * (padding - @status.size)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
GitStatus.new(ARGV.first).report
|
||||||
@@ -311,9 +311,10 @@ test_git_commit_prompt() {
|
|||||||
if [[ $shell == "zsh" ]]; then
|
if [[ $shell == "zsh" ]]; then
|
||||||
test_history="$(history)"
|
test_history="$(history)"
|
||||||
else
|
else
|
||||||
|
# Need to load history from $HISTFILE
|
||||||
|
# (Couldn't get the 'history' builtin to work during tests.)
|
||||||
test_history="$(cat $HISTFILE)"
|
test_history="$(cat $HISTFILE)"
|
||||||
fi
|
fi
|
||||||
assertIncludes "$test_history" "$commit_msg"
|
|
||||||
assertIncludes "$test_history" "git commit -m \"$dbl_escaped_msg\""
|
assertIncludes "$test_history" "git commit -m \"$dbl_escaped_msg\""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +336,7 @@ test_git_commit_prompt_with_append() {
|
|||||||
|
|
||||||
# Test the git commit prompt, by piping a commit message
|
# Test the git commit prompt, by piping a commit message
|
||||||
# instead of user input.
|
# instead of user input.
|
||||||
echo "$commit_msg" | APPEND="[ci skip]" git_commit_prompt > /dev/null
|
echo "$commit_msg" | GIT_COMMIT_MSG_SUFFIX="[ci skip]" git_commit_prompt > /dev/null
|
||||||
|
|
||||||
git_show_output=$(git show --oneline --name-only)
|
git_show_output=$(git show --oneline --name-only)
|
||||||
assertIncludes "$git_show_output" "$commit_msg \[ci skip\]"
|
assertIncludes "$git_show_output" "$commit_msg \[ci skip\]"
|
||||||
|
|||||||
Reference in New Issue
Block a user