Added git helper scripts.
This commit is contained in:
parent
58aa48f832
commit
497877514a
63
bin/executable_git-activity
Normal file
63
bin/executable_git-activity
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GIT_OPTS=""
|
||||||
|
OUTPUT_FILTER="cat" # no-op
|
||||||
|
|
||||||
|
commit_id_format=$(tput setaf 1)
|
||||||
|
date_format=$(tput bold; tput setaf 4)
|
||||||
|
author_format=$(tput setaf 2)
|
||||||
|
ref_name_format=$(tput setaf 3)
|
||||||
|
bold=$(tput bold)
|
||||||
|
reset=$(tput sgr0)
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
echo ""
|
||||||
|
echo "git activity"
|
||||||
|
echo ""
|
||||||
|
echo " See 'man git-activity' for further information"
|
||||||
|
}
|
||||||
|
|
||||||
|
# actually parse the options and do stuff
|
||||||
|
while [[ $1 = -?* ]]; do
|
||||||
|
case $1 in
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--fetch)
|
||||||
|
echo "Fetch updates"
|
||||||
|
git fetch -q
|
||||||
|
;;
|
||||||
|
-c|--count)
|
||||||
|
shift
|
||||||
|
limit=${1-"10"}
|
||||||
|
#OUTPUT_FILTER="tail -n ${limit}"
|
||||||
|
GIT_OPTS="--count=${limit}"
|
||||||
|
;;
|
||||||
|
--no-color|--no-colour)
|
||||||
|
commit_id_format=""
|
||||||
|
date_format=""
|
||||||
|
author_format=""
|
||||||
|
ref_name_format=""
|
||||||
|
bold=""
|
||||||
|
reset=""
|
||||||
|
;;
|
||||||
|
*) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# Use newline as a field separator
|
||||||
|
IFS=$(echo -en "\n\b")
|
||||||
|
|
||||||
|
# Use tac if available, otherwise tail with the possibly-not-always-available
|
||||||
|
# -r flag (for reverse output)
|
||||||
|
TAC=$(which tac || echo 'tail -r')
|
||||||
|
|
||||||
|
for line in $(git for-each-ref ${GIT_OPTS} refs/remotes --format="%(authordate:relative)|%(objectname:short)|%(authorname)|%(refname:short)|%(subject)" --sort="-authordate"); do
|
||||||
|
fields=(`echo $line | tr "|" "\n"`)
|
||||||
|
printf "${date_format}%15s${reset} ${commit_id_format}%s${reset} - ${author_format}[%s]${reset} (${ref_name_format}%s${reset}): %s\n" ${fields[*]}
|
||||||
|
done | eval $TAC # reverse sort the output to show the newest entry last
|
||||||
151
bin/executable_git-blame-colored.rb
Normal file
151
bin/executable_git-blame-colored.rb
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'date'
|
||||||
|
|
||||||
|
# Colorize string
|
||||||
|
class String
|
||||||
|
def colorize(color_code)
|
||||||
|
"\e[#{color_code}m#{self}\e[0m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Colors
|
||||||
|
@@user_color = 94
|
||||||
|
@@colors = [31, 32, 33, 35, 36]
|
||||||
|
@@index = 0
|
||||||
|
|
||||||
|
def self.user
|
||||||
|
@@user_color
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.next
|
||||||
|
color = @@colors[@@index]
|
||||||
|
|
||||||
|
# Should take care of case when more users than colors
|
||||||
|
if @@index < @@colors.count
|
||||||
|
@@index += 1
|
||||||
|
else
|
||||||
|
@@index = 0
|
||||||
|
end
|
||||||
|
color
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Authors
|
||||||
|
@@authors = {}
|
||||||
|
|
||||||
|
def self.[](key)
|
||||||
|
@@authors[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.[]=(key, value)
|
||||||
|
@@authors[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.include?(key)
|
||||||
|
@@authors.include?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.all
|
||||||
|
@@authors.collect{ |k,v| v.to_s }.join(' ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Author
|
||||||
|
def initialize(name)
|
||||||
|
@lines = 0
|
||||||
|
@name = name
|
||||||
|
if name == $current_user
|
||||||
|
@color = Colors.user
|
||||||
|
else
|
||||||
|
@color = Colors.next
|
||||||
|
end
|
||||||
|
@initials = @name.split.collect { |word| word[0] }.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def initials
|
||||||
|
@initials.colorize(@color)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_line
|
||||||
|
@lines += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"#{@name.colorize(@color)}(#{@lines})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def blame(filename)
|
||||||
|
output = "#{filename}: "
|
||||||
|
|
||||||
|
content = `git blame --line-porcelain #{filename} 2>&1`
|
||||||
|
|
||||||
|
if content =~ /^fatal:/
|
||||||
|
output << content.match(/^fatal: (.*)/)[1] + "\n\n"
|
||||||
|
return output
|
||||||
|
else
|
||||||
|
output << "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
content.split(/^[a-f0-9]{40}/).each_with_index do |line, line_number|
|
||||||
|
next if line_number == 0
|
||||||
|
|
||||||
|
# Get code line
|
||||||
|
if line =~ /^(previous|boundary)/
|
||||||
|
code = line.split("\n")[12]
|
||||||
|
else
|
||||||
|
code = line.split("\n")[11]
|
||||||
|
end
|
||||||
|
code.gsub!("\t", " ")
|
||||||
|
|
||||||
|
# Get author
|
||||||
|
if line =~ /^author /
|
||||||
|
author = line.match(/^author (.*)$/)[1]
|
||||||
|
if !Authors.include?(author)
|
||||||
|
Authors[author] = Author.new(author)
|
||||||
|
end
|
||||||
|
Authors[author].add_line
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get date
|
||||||
|
if line =~ /^author-time /
|
||||||
|
autorDate = line.match(/author-time (.*)$/)[1]
|
||||||
|
if autorDate =~ /^\d*$/
|
||||||
|
date = DateTime.strptime(autorDate,'%s')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
output << "%-3s %-4s %s %s\n" % [Authors[author].initials, line_number, date.strftime("%Y-%m-%d %H:%M:%S %Z"), code]
|
||||||
|
end
|
||||||
|
|
||||||
|
output << "\n"
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### MAIN ###
|
||||||
|
|
||||||
|
# Make sure a file was specified
|
||||||
|
filenames = ARGV
|
||||||
|
if filenames.empty?
|
||||||
|
puts "Specify a file to blame"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get the current user's name
|
||||||
|
$current_user = `git config --get user.name`.chomp
|
||||||
|
|
||||||
|
output = filenames.collect { |filename| blame(filename) }.join
|
||||||
|
output.chomp!
|
||||||
|
|
||||||
|
# Print all output
|
||||||
|
print <<EOS
|
||||||
|
|
||||||
|
#{Authors.all}
|
||||||
|
|
||||||
|
#{output}
|
||||||
|
#{Authors.all}
|
||||||
|
|
||||||
|
EOS
|
||||||
|
|
||||||
151
bin/executable_git-blamec
Normal file
151
bin/executable_git-blamec
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require 'date'
|
||||||
|
|
||||||
|
# Colorize string
|
||||||
|
class String
|
||||||
|
def colorize(color_code)
|
||||||
|
"\e[#{color_code}m#{self}\e[0m"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Colors
|
||||||
|
@@user_color = 94
|
||||||
|
@@colors = [31, 32, 33, 35, 36]
|
||||||
|
@@index = 0
|
||||||
|
|
||||||
|
def self.user
|
||||||
|
@@user_color
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.next
|
||||||
|
color = @@colors[@@index]
|
||||||
|
|
||||||
|
# Should take care of case when more users than colors
|
||||||
|
if @@index < @@colors.count
|
||||||
|
@@index += 1
|
||||||
|
else
|
||||||
|
@@index = 0
|
||||||
|
end
|
||||||
|
color
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Authors
|
||||||
|
@@authors = {}
|
||||||
|
|
||||||
|
def self.[](key)
|
||||||
|
@@authors[key]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.[]=(key, value)
|
||||||
|
@@authors[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.include?(key)
|
||||||
|
@@authors.include?(key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.all
|
||||||
|
@@authors.collect{ |k,v| v.to_s }.join(' ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Author
|
||||||
|
def initialize(name)
|
||||||
|
@lines = 0
|
||||||
|
@name = name
|
||||||
|
if name == $current_user
|
||||||
|
@color = Colors.user
|
||||||
|
else
|
||||||
|
@color = Colors.next
|
||||||
|
end
|
||||||
|
@initials = @name.split.collect { |word| word[0] }.join
|
||||||
|
end
|
||||||
|
|
||||||
|
def initials
|
||||||
|
@initials.colorize(@color)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_line
|
||||||
|
@lines += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"#{@name.colorize(@color)}(#{@lines})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def blame(filename)
|
||||||
|
output = "#{filename}: "
|
||||||
|
|
||||||
|
content = `git blame --line-porcelain #{filename} 2>&1`
|
||||||
|
|
||||||
|
if content =~ /^fatal:/
|
||||||
|
output << content.match(/^fatal: (.*)/)[1] + "\n\n"
|
||||||
|
return output
|
||||||
|
else
|
||||||
|
output << "\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
content.split(/^[a-f0-9]{40}/).each_with_index do |line, line_number|
|
||||||
|
next if line_number == 0
|
||||||
|
|
||||||
|
# Get code line
|
||||||
|
if line =~ /^(previous|boundary)/
|
||||||
|
code = line.split("\n")[12]
|
||||||
|
else
|
||||||
|
code = line.split("\n")[11]
|
||||||
|
end
|
||||||
|
code.gsub!("\t", " ")
|
||||||
|
|
||||||
|
# Get author
|
||||||
|
if line =~ /^author /
|
||||||
|
author = line.match(/^author (.*)$/)[1]
|
||||||
|
if !Authors.include?(author)
|
||||||
|
Authors[author] = Author.new(author)
|
||||||
|
end
|
||||||
|
Authors[author].add_line
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get date
|
||||||
|
if line =~ /^author-time /
|
||||||
|
autorDate = line.match(/author-time (.*)$/)[1]
|
||||||
|
if autorDate =~ /^\d*$/
|
||||||
|
date = DateTime.strptime(autorDate,'%s')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
output << "%-3s %-4s %s %s\n" % [Authors[author].initials, line_number, date.strftime("%Y-%m-%d %H:%M:%S %Z"), code]
|
||||||
|
end
|
||||||
|
|
||||||
|
output << "\n"
|
||||||
|
return output
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
### MAIN ###
|
||||||
|
|
||||||
|
# Make sure a file was specified
|
||||||
|
filenames = ARGV
|
||||||
|
if filenames.empty?
|
||||||
|
puts "Specify a file to blame"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Get the current user's name
|
||||||
|
$current_user = `git config --get user.name`.chomp
|
||||||
|
|
||||||
|
output = filenames.collect { |filename| blame(filename) }.join
|
||||||
|
output.chomp!
|
||||||
|
|
||||||
|
# Print all output
|
||||||
|
print <<EOS
|
||||||
|
|
||||||
|
#{Authors.all}
|
||||||
|
|
||||||
|
#{output}
|
||||||
|
#{Authors.all}
|
||||||
|
|
||||||
|
EOS
|
||||||
|
|
||||||
3
bin/executable_git-branchinfo
Normal file
3
bin/executable_git-branchinfo
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
for k in `git branch | perl -pe s/^..//`; do echo -e `git show --pretty=format:"%Cgreen%ci %Cblue%cr%Creset" $k -- | head -n 1`\\t$k; done | sort -r
|
||||||
21
bin/executable_git-hook
Normal file
21
bin/executable_git-hook
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
root=$(git rev-parse --show-toplevel)
|
||||||
|
echo "root dir: $root"
|
||||||
|
|
||||||
|
echo "enabled hooks: "
|
||||||
|
for f in $root/.git/hooks/*; do
|
||||||
|
if [[ ${f: -7} != ".sample" ]]; then
|
||||||
|
echo " $(basename $f)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -L "$root/.git/hooks/pre-commit" ]]; then
|
||||||
|
echo "pre-commit already linked"
|
||||||
|
elif [[ -f "$root/.git/hooks/pre-commit" ]]; then
|
||||||
|
echo "pre-commit already exists (as regular file)"
|
||||||
|
else
|
||||||
|
echo "linking pre-commit $HOME/.git_hooks/pre-commit"
|
||||||
|
ln -s $HOME/.git_hooks/pre-commit $root/.git/hooks/pre-commit
|
||||||
|
fi
|
||||||
|
|
||||||
419
bin/executable_git-quick-stats
Normal file
419
bin/executable_git-quick-stats
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#
|
||||||
|
# Source: https://github.com/arzzen/git-quick-stats
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# MIT License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2017 Lukáš Mešťan
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
#
|
||||||
|
set -o nounset
|
||||||
|
set -o errexit
|
||||||
|
|
||||||
|
_since=${_GIT_SINCE:-}
|
||||||
|
if [ ! -z ${_since} ]
|
||||||
|
then _since="--since=$_since"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_until=${_GIT_UNTIL:-}
|
||||||
|
if [ ! -z ${_until} ]
|
||||||
|
then _until="--until=$_until"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_limit=${_GIT_LIMIT:-}
|
||||||
|
if [ ! -z ${_limit} ]
|
||||||
|
then _limit=$_limit
|
||||||
|
else
|
||||||
|
_limit=10
|
||||||
|
fi
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
NORMAL=`echo "\033[m"`
|
||||||
|
MENU=`echo "\033[36m"`
|
||||||
|
NUMBER=`echo "\033[33m"`
|
||||||
|
FGRED=`echo "\033[41m"`
|
||||||
|
RED_TEXT=`echo "\033[31m"`
|
||||||
|
ENTER_LINE=`echo "\033[33m"`
|
||||||
|
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${RED_TEXT} Generate: ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 1)${MENU} Contribution stats (by author) ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 2)${MENU} Git changelogs (last $_limit)${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 3)${MENU} My daily status ${NORMAL}"
|
||||||
|
echo -e "${RED_TEXT} List: ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 4)${MENU} Branch tree view (last $_limit)${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 5)${MENU} All branches (sorted by most recent commit) ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 6)${MENU} All contributors (sorted by name) ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 7)${MENU} Git commits per author ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 8)${MENU} Git commits per date ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 9)${MENU} Git commits per month ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 10)${MENU} Git commits per weekday ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 11)${MENU} Git commits per hour ${NORMAL}"
|
||||||
|
echo -e "${RED_TEXT} Suggest: ${NORMAL}"
|
||||||
|
echo -e "${MENU} ${NUMBER} 12)${MENU} Code reviewers (based on git history) ${NORMAL}"
|
||||||
|
echo -e ""
|
||||||
|
echo -e "${ENTER_LINE}Please enter a menu option or ${RED_TEXT}press enter to exit. ${NORMAL}"
|
||||||
|
read opt
|
||||||
|
}
|
||||||
|
|
||||||
|
function option_picked() {
|
||||||
|
COLOR='\033[01;31m'
|
||||||
|
RESET='\033[00;00m'
|
||||||
|
MESSAGE=${@:-"${RESET}Error: No message passed"}
|
||||||
|
echo -e "${COLOR}${MESSAGE}${RESET}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailedGitStats() {
|
||||||
|
option_picked "Contribution stats (by author):"
|
||||||
|
|
||||||
|
git log --no-merges --numstat --pretty="format:commit %H%nAuthor: %an <%ae>%nDate: %ad%n%n%w(0,4,4)%B%n" $_since $_until | LC_ALL=C awk '
|
||||||
|
function printStats(author) {
|
||||||
|
printf "\t%s:\n", author
|
||||||
|
|
||||||
|
if( more["total"] > 0 ) {
|
||||||
|
printf "\t insertions: %d (%.0f%%)\n", more[author], (more[author] / more["total"] * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if( less["total"] > 0 ) {
|
||||||
|
printf "\t deletions: %d (%.0f%%)\n", less[author], (less[author] / less["total"] * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if( file["total"] > 0 ) {
|
||||||
|
printf "\t files: %d (%.0f%%)\n", file[author], (file[author] / file["total"] * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(commits["total"] > 0) {
|
||||||
|
printf "\t commits: %d (%.0f%%)\n", commits[author], (commits[author] / commits["total"] * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( first[author] != "" ) {
|
||||||
|
printf "\t first commit: %s\n", first[author]
|
||||||
|
printf "\t last commit: %s\n", last[author]
|
||||||
|
}
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
/^Author:/ {
|
||||||
|
author = $2 " " $3
|
||||||
|
commits[author] += 1
|
||||||
|
commits["total"] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/^Date:/ {
|
||||||
|
$1="";
|
||||||
|
first[author] = substr($0, 2)
|
||||||
|
if(last[author] == "" ) { last[author] = first[author] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/^[0-9]/ {
|
||||||
|
more[author] += $1
|
||||||
|
less[author] += $2
|
||||||
|
file[author] += 1
|
||||||
|
|
||||||
|
more["total"] += $1
|
||||||
|
less["total"] += $2
|
||||||
|
file["total"] += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
END {
|
||||||
|
for (author in commits) {
|
||||||
|
if (author != "total") {
|
||||||
|
printStats(author)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printStats("total")
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestReviewers() {
|
||||||
|
option_picked "Suggested code reviewers (based on git history):"
|
||||||
|
git log --no-merges $_since $_until --pretty=%an $* | head -n 100 | sort | uniq -c | sort -nr | LC_ALL=C awk '
|
||||||
|
{ args[NR] = $0; }
|
||||||
|
END {
|
||||||
|
for (i = 1; i <= NR; ++i) {
|
||||||
|
printf "%s\n", args[i]
|
||||||
|
}
|
||||||
|
}' | column -t -s,
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitsByMonth() {
|
||||||
|
option_picked "Git commits by month:"
|
||||||
|
echo -e "\tmonth\tsum"
|
||||||
|
for i in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
|
||||||
|
do
|
||||||
|
echo -en "\t$i\t"
|
||||||
|
echo $(git shortlog -n --no-merges --format='%ad %s' $_since $_until | grep " $i " | wc -l)
|
||||||
|
done | awk '{
|
||||||
|
count[$1] = $2
|
||||||
|
total += $2
|
||||||
|
}
|
||||||
|
END{
|
||||||
|
for (month in count) {
|
||||||
|
s="";
|
||||||
|
percent = ((count[month] / total) * 100) / 1.25;
|
||||||
|
for (i = 1; i <= percent; ++i) {
|
||||||
|
s=s"="
|
||||||
|
}
|
||||||
|
printf( "\t%s\t%-0s\t|%s\n", month, count[month], s );
|
||||||
|
}
|
||||||
|
}' | LC_TIME="en_EN.UTF-8" sort -M
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitsByWeekday() {
|
||||||
|
option_picked "Git commits by weekday:"
|
||||||
|
echo -e "\tday\tsum"
|
||||||
|
for i in Mon Tue Wed Thu Fri Sat Sun
|
||||||
|
do
|
||||||
|
echo -en "\t$i\t"
|
||||||
|
echo $(git shortlog -n --no-merges --format='%ad %s' $_since $_until | grep "$i " | wc -l)
|
||||||
|
done | awk '{
|
||||||
|
|
||||||
|
}
|
||||||
|
NR == FNR {
|
||||||
|
count[$1] = $2;
|
||||||
|
total += $2;
|
||||||
|
next
|
||||||
|
}
|
||||||
|
END{
|
||||||
|
|
||||||
|
for (day in count) {
|
||||||
|
s="";
|
||||||
|
percent = ((count[day] / total) * 100) / 1.25;
|
||||||
|
for (i = 1; i <= percent; ++i) {
|
||||||
|
s=s"="
|
||||||
|
}
|
||||||
|
printf( "\t%s\t%-0s\t|%s\n", day, count[day], s );
|
||||||
|
}
|
||||||
|
}' | sort -k 2 -n -r
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitsByHour() {
|
||||||
|
option_picked "Git commits by hour:"
|
||||||
|
echo -e "\thour\tsum"
|
||||||
|
for i in `seq -w 0 23`
|
||||||
|
do
|
||||||
|
echo -ne "\t$i\t"
|
||||||
|
echo $(git shortlog -n --no-merges --format='%ad %s' $_since $_until | grep " $i:" | wc -l)
|
||||||
|
done | awk '{
|
||||||
|
count[$1] = $2
|
||||||
|
total += $2
|
||||||
|
}
|
||||||
|
END{
|
||||||
|
for (hour in count) {
|
||||||
|
s="";
|
||||||
|
percent = ((count[hour] / total) * 100) / 1.25;
|
||||||
|
for (i = 1; i <= percent; ++i) {
|
||||||
|
s=s"="
|
||||||
|
}
|
||||||
|
printf( "\t%s\t%-0s\t|%s\n", hour, count[hour], s );
|
||||||
|
}
|
||||||
|
}' | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitsPerDay() {
|
||||||
|
option_picked "Git commits per date:";
|
||||||
|
git log --no-merges $_since $_until --date=short --format='%ad' | sort | uniq -c
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitsPerAuthor() {
|
||||||
|
option_picked "Git commits per author:"
|
||||||
|
git shortlog $_since $_until --no-merges -n -s | sort -nr | LC_ALL=C awk '
|
||||||
|
{ args[NR] = $0; sum += $0 }
|
||||||
|
END {
|
||||||
|
for (i = 1; i <= NR; ++i) {
|
||||||
|
printf "%s,%2.1f%%\n", args[i], 100 * args[i] / sum
|
||||||
|
}
|
||||||
|
}' | column -t -s,
|
||||||
|
}
|
||||||
|
|
||||||
|
function myDailyStats() {
|
||||||
|
option_picked "My daily status:"
|
||||||
|
git diff --shortstat '@{0 day ago}' | sort -nr | tr ',' '\n' | LC_ALL=C awk '
|
||||||
|
{ args[NR] = $0; }
|
||||||
|
END {
|
||||||
|
for (i = 1; i <= NR; ++i) {
|
||||||
|
printf "\t%s\n", args[i]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo -e "\t" $(git log --author="$(git config user.name)" --no-merges --since=$(date "+%Y-%m-%dT00:00:00") --until=$(date "+%Y-%m-%dT23:59:59") --reverse | grep commit | wc -l) "commits"
|
||||||
|
}
|
||||||
|
|
||||||
|
function contributors() {
|
||||||
|
option_picked "All contributors (sorted by name):"
|
||||||
|
git log --no-merges $_since $_until --format='%aN' | sort -u | cat -n
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchTree() {
|
||||||
|
option_picked "Branching tree view:"
|
||||||
|
git log --graph --abbrev-commit $_since $_until --decorate --format=format:'--+ Commit: %h %n | Date: %aD (%ar) %n'' | Message: %s %d %n'' + Author: %an %n' --all | head -n $((_limit*5))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function branchesByDate() {
|
||||||
|
option_picked "All branches (sorted by most recent commit):"
|
||||||
|
git for-each-ref --sort=committerdate refs/heads/ --format='[%(authordate:relative)] %(authorname) %(refname:short)' | cat -n
|
||||||
|
}
|
||||||
|
|
||||||
|
function changelogs() {
|
||||||
|
option_picked "Git changelogs:"
|
||||||
|
NEXT=$(date +%F)
|
||||||
|
|
||||||
|
git log --no-merges --format="%cd" --date=short $_since $_until | sort -u -r | head -n $_limit | while read DATE ; do
|
||||||
|
echo
|
||||||
|
echo "[$DATE]"
|
||||||
|
GIT_PAGER=cat git log --no-merges --format=" * %s" --since=$DATE --until=$NEXT
|
||||||
|
NEXT=$DATE
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if we are currently in a git repo.
|
||||||
|
git rev-parse --is-inside-work-tree > /dev/null
|
||||||
|
|
||||||
|
if [ $# -eq 1 ]
|
||||||
|
then
|
||||||
|
case $1 in
|
||||||
|
"suggestReviewers")
|
||||||
|
suggestReviewers
|
||||||
|
;;
|
||||||
|
"detailedGitStats")
|
||||||
|
detailedGitStats
|
||||||
|
;;
|
||||||
|
"branchTree")
|
||||||
|
branchTree
|
||||||
|
;;
|
||||||
|
"commitsPerDay")
|
||||||
|
commitsPerDay
|
||||||
|
;;
|
||||||
|
"commitsPerAuthor")
|
||||||
|
commitsPerAuthor
|
||||||
|
;;
|
||||||
|
"myDailyStats")
|
||||||
|
myDailyStats
|
||||||
|
;;
|
||||||
|
"contributors")
|
||||||
|
contributors
|
||||||
|
;;
|
||||||
|
"branchesByDate")
|
||||||
|
branchesByDate
|
||||||
|
;;
|
||||||
|
"changelogs")
|
||||||
|
changelogs
|
||||||
|
;;
|
||||||
|
"commitsByWeekday")
|
||||||
|
commitsByWeekday
|
||||||
|
;;
|
||||||
|
"commitsByHour")
|
||||||
|
commitsByHour
|
||||||
|
;;
|
||||||
|
"commitsByMonth")
|
||||||
|
commitsByMonth
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid argument. Possible arguments: suggestReviewers, detailedGitStats, commitsPerDay, commitsByMonth, commitsByWeekday, commitsByHour, commitsPerAuthor, myDailyStats, contributors, branchTree, branchesByDate, changelogs"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit 0;
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $# -gt 1 ]
|
||||||
|
then
|
||||||
|
echo "Usage: git quick-stats <optional-command-to-execute-directly>";
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
|
||||||
|
clear
|
||||||
|
show_menu
|
||||||
|
|
||||||
|
while [ opt != '' ]
|
||||||
|
do
|
||||||
|
if [[ $opt = "" ]]; then
|
||||||
|
exit;
|
||||||
|
else
|
||||||
|
clear
|
||||||
|
case $opt in
|
||||||
|
1)
|
||||||
|
detailedGitStats
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
changelogs
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
myDailyStats
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
branchTree
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
branchesByDate
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
contributors
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
commitsPerAuthor
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
commitsPerDay
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
commitsByMonth
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
10)
|
||||||
|
commitsByWeekday
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
11)
|
||||||
|
commitsByHour
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
12)
|
||||||
|
suggestReviewers
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
q)
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
\n)
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
clear
|
||||||
|
option_picked "Pick an option from the menu"
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
864
bin/executable_git-subtree
Normal file
864
bin/executable_git-subtree
Normal file
@ -0,0 +1,864 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# git-subtree.sh: split/join git repositories in subdirectories of this one
|
||||||
|
#
|
||||||
|
# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
|
||||||
|
#
|
||||||
|
if test $# -eq 0
|
||||||
|
then
|
||||||
|
set -- -h
|
||||||
|
fi
|
||||||
|
OPTS_SPEC="\
|
||||||
|
git subtree add --prefix=<prefix> <commit>
|
||||||
|
git subtree add --prefix=<prefix> <repository> <ref>
|
||||||
|
git subtree merge --prefix=<prefix> <commit>
|
||||||
|
git subtree pull --prefix=<prefix> <repository> <ref>
|
||||||
|
git subtree push --prefix=<prefix> <repository> <ref>
|
||||||
|
git subtree split --prefix=<prefix> <commit...>
|
||||||
|
--
|
||||||
|
h,help show the help
|
||||||
|
q quiet
|
||||||
|
d show debug messages
|
||||||
|
P,prefix= the name of the subdir to split out
|
||||||
|
m,message= use the given message as the commit message for the merge commit
|
||||||
|
options for 'split'
|
||||||
|
annotate= add a prefix to commit message of new commits
|
||||||
|
b,branch= create a new branch from the split subtree
|
||||||
|
ignore-joins ignore prior --rejoin commits
|
||||||
|
onto= try connecting new tree to an existing one
|
||||||
|
rejoin merge the new branch back into HEAD
|
||||||
|
options for 'add', 'merge', and 'pull'
|
||||||
|
squash merge subtree changes as a single commit
|
||||||
|
"
|
||||||
|
eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
|
||||||
|
|
||||||
|
PATH=$PATH:$(git --exec-path)
|
||||||
|
. git-sh-setup
|
||||||
|
|
||||||
|
require_work_tree
|
||||||
|
|
||||||
|
quiet=
|
||||||
|
branch=
|
||||||
|
debug=
|
||||||
|
command=
|
||||||
|
onto=
|
||||||
|
rejoin=
|
||||||
|
ignore_joins=
|
||||||
|
annotate=
|
||||||
|
squash=
|
||||||
|
message=
|
||||||
|
prefix=
|
||||||
|
|
||||||
|
debug () {
|
||||||
|
if test -n "$debug"
|
||||||
|
then
|
||||||
|
printf "%s\n" "$*" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
say () {
|
||||||
|
if test -z "$quiet"
|
||||||
|
then
|
||||||
|
printf "%s\n" "$*" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
progress () {
|
||||||
|
if test -z "$quiet"
|
||||||
|
then
|
||||||
|
printf "%s\r" "$*" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert () {
|
||||||
|
if ! "$@"
|
||||||
|
then
|
||||||
|
die "assertion failed: " "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while test $# -gt 0
|
||||||
|
do
|
||||||
|
opt="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$opt" in
|
||||||
|
-q)
|
||||||
|
quiet=1
|
||||||
|
;;
|
||||||
|
-d)
|
||||||
|
debug=1
|
||||||
|
;;
|
||||||
|
--annotate)
|
||||||
|
annotate="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-annotate)
|
||||||
|
annotate=
|
||||||
|
;;
|
||||||
|
-b)
|
||||||
|
branch="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-P)
|
||||||
|
prefix="${1%/}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-m)
|
||||||
|
message="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-prefix)
|
||||||
|
prefix=
|
||||||
|
;;
|
||||||
|
--onto)
|
||||||
|
onto="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-onto)
|
||||||
|
onto=
|
||||||
|
;;
|
||||||
|
--rejoin)
|
||||||
|
rejoin=1
|
||||||
|
;;
|
||||||
|
--no-rejoin)
|
||||||
|
rejoin=
|
||||||
|
;;
|
||||||
|
--ignore-joins)
|
||||||
|
ignore_joins=1
|
||||||
|
;;
|
||||||
|
--no-ignore-joins)
|
||||||
|
ignore_joins=
|
||||||
|
;;
|
||||||
|
--squash)
|
||||||
|
squash=1
|
||||||
|
;;
|
||||||
|
--no-squash)
|
||||||
|
squash=
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unexpected option: $opt"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
command="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
add|merge|pull)
|
||||||
|
default=
|
||||||
|
;;
|
||||||
|
split|push)
|
||||||
|
default="--default HEAD"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown command '$command'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if test -z "$prefix"
|
||||||
|
then
|
||||||
|
die "You must provide the --prefix option."
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
add)
|
||||||
|
test -e "$prefix" &&
|
||||||
|
die "prefix '$prefix' already exists."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
test -e "$prefix" ||
|
||||||
|
die "'$prefix' does not exist; use 'git subtree add'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
dir="$(dirname "$prefix/.")"
|
||||||
|
|
||||||
|
if test "$command" != "pull" &&
|
||||||
|
test "$command" != "add" &&
|
||||||
|
test "$command" != "push"
|
||||||
|
then
|
||||||
|
revs=$(git rev-parse $default --revs-only "$@") || exit $?
|
||||||
|
dirs=$(git rev-parse --no-revs --no-flags "$@") || exit $?
|
||||||
|
if test -n "$dirs"
|
||||||
|
then
|
||||||
|
die "Error: Use --prefix instead of bare filenames."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
debug "command: {$command}"
|
||||||
|
debug "quiet: {$quiet}"
|
||||||
|
debug "revs: {$revs}"
|
||||||
|
debug "dir: {$dir}"
|
||||||
|
debug "opts: {$*}"
|
||||||
|
debug
|
||||||
|
|
||||||
|
cache_setup () {
|
||||||
|
cachedir="$GIT_DIR/subtree-cache/$$"
|
||||||
|
rm -rf "$cachedir" ||
|
||||||
|
die "Can't delete old cachedir: $cachedir"
|
||||||
|
mkdir -p "$cachedir" ||
|
||||||
|
die "Can't create new cachedir: $cachedir"
|
||||||
|
mkdir -p "$cachedir/notree" ||
|
||||||
|
die "Can't create new cachedir: $cachedir/notree"
|
||||||
|
debug "Using cachedir: $cachedir" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_get () {
|
||||||
|
for oldrev in "$@"
|
||||||
|
do
|
||||||
|
if test -r "$cachedir/$oldrev"
|
||||||
|
then
|
||||||
|
read newrev <"$cachedir/$oldrev"
|
||||||
|
echo $newrev
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_miss () {
|
||||||
|
for oldrev in "$@"
|
||||||
|
do
|
||||||
|
if ! test -r "$cachedir/$oldrev"
|
||||||
|
then
|
||||||
|
echo $oldrev
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
check_parents () {
|
||||||
|
missed=$(cache_miss "$@")
|
||||||
|
for miss in $missed
|
||||||
|
do
|
||||||
|
if ! test -r "$cachedir/notree/$miss"
|
||||||
|
then
|
||||||
|
debug " incorrect order: $miss"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
set_notree () {
|
||||||
|
echo "1" > "$cachedir/notree/$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_set () {
|
||||||
|
oldrev="$1"
|
||||||
|
newrev="$2"
|
||||||
|
if test "$oldrev" != "latest_old" &&
|
||||||
|
test "$oldrev" != "latest_new" &&
|
||||||
|
test -e "$cachedir/$oldrev"
|
||||||
|
then
|
||||||
|
die "cache for $oldrev already exists!"
|
||||||
|
fi
|
||||||
|
echo "$newrev" >"$cachedir/$oldrev"
|
||||||
|
}
|
||||||
|
|
||||||
|
rev_exists () {
|
||||||
|
if git rev-parse "$1" >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rev_is_descendant_of_branch () {
|
||||||
|
newrev="$1"
|
||||||
|
branch="$2"
|
||||||
|
branch_hash=$(git rev-parse "$branch")
|
||||||
|
match=$(git rev-list -1 "$branch_hash" "^$newrev")
|
||||||
|
|
||||||
|
if test -z "$match"
|
||||||
|
then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# if a commit doesn't have a parent, this might not work. But we only want
|
||||||
|
# to remove the parent from the rev-list, and since it doesn't exist, it won't
|
||||||
|
# be there anyway, so do nothing in that case.
|
||||||
|
try_remove_previous () {
|
||||||
|
if rev_exists "$1^"
|
||||||
|
then
|
||||||
|
echo "^$1^"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
find_latest_squash () {
|
||||||
|
debug "Looking for latest squash ($dir)..."
|
||||||
|
dir="$1"
|
||||||
|
sq=
|
||||||
|
main=
|
||||||
|
sub=
|
||||||
|
git log --grep="^git-subtree-dir: $dir/*\$" \
|
||||||
|
--pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
|
||||||
|
while read a b junk
|
||||||
|
do
|
||||||
|
debug "$a $b $junk"
|
||||||
|
debug "{{$sq/$main/$sub}}"
|
||||||
|
case "$a" in
|
||||||
|
START)
|
||||||
|
sq="$b"
|
||||||
|
;;
|
||||||
|
git-subtree-mainline:)
|
||||||
|
main="$b"
|
||||||
|
;;
|
||||||
|
git-subtree-split:)
|
||||||
|
sub="$(git rev-parse "$b^0")" ||
|
||||||
|
die "could not rev-parse split hash $b from commit $sq"
|
||||||
|
;;
|
||||||
|
END)
|
||||||
|
if test -n "$sub"
|
||||||
|
then
|
||||||
|
if test -n "$main"
|
||||||
|
then
|
||||||
|
# a rejoin commit?
|
||||||
|
# Pretend its sub was a squash.
|
||||||
|
sq="$sub"
|
||||||
|
fi
|
||||||
|
debug "Squash found: $sq $sub"
|
||||||
|
echo "$sq" "$sub"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sq=
|
||||||
|
main=
|
||||||
|
sub=
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
find_existing_splits () {
|
||||||
|
debug "Looking for prior splits..."
|
||||||
|
dir="$1"
|
||||||
|
revs="$2"
|
||||||
|
main=
|
||||||
|
sub=
|
||||||
|
git log --grep="^git-subtree-dir: $dir/*\$" \
|
||||||
|
--pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
|
||||||
|
while read a b junk
|
||||||
|
do
|
||||||
|
case "$a" in
|
||||||
|
START)
|
||||||
|
sq="$b"
|
||||||
|
;;
|
||||||
|
git-subtree-mainline:)
|
||||||
|
main="$b"
|
||||||
|
;;
|
||||||
|
git-subtree-split:)
|
||||||
|
sub="$(git rev-parse "$b^0")" ||
|
||||||
|
die "could not rev-parse split hash $b from commit $sq"
|
||||||
|
;;
|
||||||
|
END)
|
||||||
|
debug " Main is: '$main'"
|
||||||
|
if test -z "$main" -a -n "$sub"
|
||||||
|
then
|
||||||
|
# squash commits refer to a subtree
|
||||||
|
debug " Squash: $sq from $sub"
|
||||||
|
cache_set "$sq" "$sub"
|
||||||
|
fi
|
||||||
|
if test -n "$main" -a -n "$sub"
|
||||||
|
then
|
||||||
|
debug " Prior: $main -> $sub"
|
||||||
|
cache_set $main $sub
|
||||||
|
cache_set $sub $sub
|
||||||
|
try_remove_previous "$main"
|
||||||
|
try_remove_previous "$sub"
|
||||||
|
fi
|
||||||
|
main=
|
||||||
|
sub=
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_commit () {
|
||||||
|
# We're going to set some environment vars here, so
|
||||||
|
# do it in a subshell to get rid of them safely later
|
||||||
|
debug copy_commit "{$1}" "{$2}" "{$3}"
|
||||||
|
git log -1 --pretty=format:'%an%n%ae%n%aD%n%cn%n%ce%n%cD%n%B' "$1" |
|
||||||
|
(
|
||||||
|
read GIT_AUTHOR_NAME
|
||||||
|
read GIT_AUTHOR_EMAIL
|
||||||
|
read GIT_AUTHOR_DATE
|
||||||
|
read GIT_COMMITTER_NAME
|
||||||
|
read GIT_COMMITTER_EMAIL
|
||||||
|
read GIT_COMMITTER_DATE
|
||||||
|
export GIT_AUTHOR_NAME \
|
||||||
|
GIT_AUTHOR_EMAIL \
|
||||||
|
GIT_AUTHOR_DATE \
|
||||||
|
GIT_COMMITTER_NAME \
|
||||||
|
GIT_COMMITTER_EMAIL \
|
||||||
|
GIT_COMMITTER_DATE
|
||||||
|
(
|
||||||
|
printf "%s" "$annotate"
|
||||||
|
cat
|
||||||
|
) |
|
||||||
|
git commit-tree "$2" $3 # reads the rest of stdin
|
||||||
|
) || die "Can't copy commit $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_msg () {
|
||||||
|
dir="$1"
|
||||||
|
latest_old="$2"
|
||||||
|
latest_new="$3"
|
||||||
|
if test -n "$message"
|
||||||
|
then
|
||||||
|
commit_message="$message"
|
||||||
|
else
|
||||||
|
commit_message="Add '$dir/' from commit '$latest_new'"
|
||||||
|
fi
|
||||||
|
cat <<-EOF
|
||||||
|
$commit_message
|
||||||
|
|
||||||
|
git-subtree-dir: $dir
|
||||||
|
git-subtree-mainline: $latest_old
|
||||||
|
git-subtree-split: $latest_new
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
add_squashed_msg () {
|
||||||
|
if test -n "$message"
|
||||||
|
then
|
||||||
|
echo "$message"
|
||||||
|
else
|
||||||
|
echo "Merge commit '$1' as '$2'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rejoin_msg () {
|
||||||
|
dir="$1"
|
||||||
|
latest_old="$2"
|
||||||
|
latest_new="$3"
|
||||||
|
if test -n "$message"
|
||||||
|
then
|
||||||
|
commit_message="$message"
|
||||||
|
else
|
||||||
|
commit_message="Split '$dir/' into commit '$latest_new'"
|
||||||
|
fi
|
||||||
|
cat <<-EOF
|
||||||
|
$commit_message
|
||||||
|
|
||||||
|
git-subtree-dir: $dir
|
||||||
|
git-subtree-mainline: $latest_old
|
||||||
|
git-subtree-split: $latest_new
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
squash_msg () {
|
||||||
|
dir="$1"
|
||||||
|
oldsub="$2"
|
||||||
|
newsub="$3"
|
||||||
|
newsub_short=$(git rev-parse --short "$newsub")
|
||||||
|
|
||||||
|
if test -n "$oldsub"
|
||||||
|
then
|
||||||
|
oldsub_short=$(git rev-parse --short "$oldsub")
|
||||||
|
echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
|
||||||
|
echo
|
||||||
|
git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
|
||||||
|
git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
|
||||||
|
else
|
||||||
|
echo "Squashed '$dir/' content from commit $newsub_short"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "git-subtree-dir: $dir"
|
||||||
|
echo "git-subtree-split: $newsub"
|
||||||
|
}
|
||||||
|
|
||||||
|
toptree_for_commit () {
|
||||||
|
commit="$1"
|
||||||
|
git log -1 --pretty=format:'%T' "$commit" -- || exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
subtree_for_commit () {
|
||||||
|
commit="$1"
|
||||||
|
dir="$2"
|
||||||
|
git ls-tree "$commit" -- "$dir" |
|
||||||
|
while read mode type tree name
|
||||||
|
do
|
||||||
|
assert test "$name" = "$dir"
|
||||||
|
assert test "$type" = "tree" -o "$type" = "commit"
|
||||||
|
test "$type" = "commit" && continue # ignore submodules
|
||||||
|
echo $tree
|
||||||
|
break
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
tree_changed () {
|
||||||
|
tree=$1
|
||||||
|
shift
|
||||||
|
if test $# -ne 1
|
||||||
|
then
|
||||||
|
return 0 # weird parents, consider it changed
|
||||||
|
else
|
||||||
|
ptree=$(toptree_for_commit $1)
|
||||||
|
if test "$ptree" != "$tree"
|
||||||
|
then
|
||||||
|
return 0 # changed
|
||||||
|
else
|
||||||
|
return 1 # not changed
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
new_squash_commit () {
|
||||||
|
old="$1"
|
||||||
|
oldsub="$2"
|
||||||
|
newsub="$3"
|
||||||
|
tree=$(toptree_for_commit $newsub) || exit $?
|
||||||
|
if test -n "$old"
|
||||||
|
then
|
||||||
|
squash_msg "$dir" "$oldsub" "$newsub" |
|
||||||
|
git commit-tree "$tree" -p "$old" || exit $?
|
||||||
|
else
|
||||||
|
squash_msg "$dir" "" "$newsub" |
|
||||||
|
git commit-tree "$tree" || exit $?
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_or_skip () {
|
||||||
|
rev="$1"
|
||||||
|
tree="$2"
|
||||||
|
newparents="$3"
|
||||||
|
assert test -n "$tree"
|
||||||
|
|
||||||
|
identical=
|
||||||
|
nonidentical=
|
||||||
|
p=
|
||||||
|
gotparents=
|
||||||
|
for parent in $newparents
|
||||||
|
do
|
||||||
|
ptree=$(toptree_for_commit $parent) || exit $?
|
||||||
|
test -z "$ptree" && continue
|
||||||
|
if test "$ptree" = "$tree"
|
||||||
|
then
|
||||||
|
# an identical parent could be used in place of this rev.
|
||||||
|
identical="$parent"
|
||||||
|
else
|
||||||
|
nonidentical="$parent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# sometimes both old parents map to the same newparent;
|
||||||
|
# eliminate duplicates
|
||||||
|
is_new=1
|
||||||
|
for gp in $gotparents
|
||||||
|
do
|
||||||
|
if test "$gp" = "$parent"
|
||||||
|
then
|
||||||
|
is_new=
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if test -n "$is_new"
|
||||||
|
then
|
||||||
|
gotparents="$gotparents $parent"
|
||||||
|
p="$p -p $parent"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
copycommit=
|
||||||
|
if test -n "$identical" && test -n "$nonidentical"
|
||||||
|
then
|
||||||
|
extras=$(git rev-list --count $identical..$nonidentical)
|
||||||
|
if test "$extras" -ne 0
|
||||||
|
then
|
||||||
|
# we need to preserve history along the other branch
|
||||||
|
copycommit=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if test -n "$identical" && test -z "$copycommit"
|
||||||
|
then
|
||||||
|
echo $identical
|
||||||
|
else
|
||||||
|
copy_commit "$rev" "$tree" "$p" || exit $?
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_clean () {
|
||||||
|
if ! git diff-index HEAD --exit-code --quiet 2>&1
|
||||||
|
then
|
||||||
|
die "Working tree has modifications. Cannot add."
|
||||||
|
fi
|
||||||
|
if ! git diff-index --cached HEAD --exit-code --quiet 2>&1
|
||||||
|
then
|
||||||
|
die "Index has modifications. Cannot add."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_valid_ref_format () {
|
||||||
|
git check-ref-format "refs/heads/$1" ||
|
||||||
|
die "'$1' does not look like a ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_add () {
|
||||||
|
if test -e "$dir"
|
||||||
|
then
|
||||||
|
die "'$dir' already exists. Cannot add."
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_clean
|
||||||
|
|
||||||
|
if test $# -eq 1
|
||||||
|
then
|
||||||
|
git rev-parse -q --verify "$1^{commit}" >/dev/null ||
|
||||||
|
die "'$1' does not refer to a commit"
|
||||||
|
|
||||||
|
cmd_add_commit "$@"
|
||||||
|
|
||||||
|
elif test $# -eq 2
|
||||||
|
then
|
||||||
|
# Technically we could accept a refspec here but we're
|
||||||
|
# just going to turn around and add FETCH_HEAD under the
|
||||||
|
# specified directory. Allowing a refspec might be
|
||||||
|
# misleading because we won't do anything with any other
|
||||||
|
# branches fetched via the refspec.
|
||||||
|
ensure_valid_ref_format "$2"
|
||||||
|
|
||||||
|
cmd_add_repository "$@"
|
||||||
|
else
|
||||||
|
say "error: parameters were '$@'"
|
||||||
|
die "Provide either a commit or a repository and commit."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_add_repository () {
|
||||||
|
echo "git fetch" "$@"
|
||||||
|
repository=$1
|
||||||
|
refspec=$2
|
||||||
|
git fetch "$@" || exit $?
|
||||||
|
revs=FETCH_HEAD
|
||||||
|
set -- $revs
|
||||||
|
cmd_add_commit "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_add_commit () {
|
||||||
|
revs=$(git rev-parse $default --revs-only "$@") || exit $?
|
||||||
|
set -- $revs
|
||||||
|
rev="$1"
|
||||||
|
|
||||||
|
debug "Adding $dir as '$rev'..."
|
||||||
|
git read-tree --prefix="$dir" $rev || exit $?
|
||||||
|
git checkout -- "$dir" || exit $?
|
||||||
|
tree=$(git write-tree) || exit $?
|
||||||
|
|
||||||
|
headrev=$(git rev-parse HEAD) || exit $?
|
||||||
|
if test -n "$headrev" && test "$headrev" != "$rev"
|
||||||
|
then
|
||||||
|
headp="-p $headrev"
|
||||||
|
else
|
||||||
|
headp=
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -n "$squash"
|
||||||
|
then
|
||||||
|
rev=$(new_squash_commit "" "" "$rev") || exit $?
|
||||||
|
commit=$(add_squashed_msg "$rev" "$dir" |
|
||||||
|
git commit-tree "$tree" $headp -p "$rev") || exit $?
|
||||||
|
else
|
||||||
|
revp=$(peel_committish "$rev") &&
|
||||||
|
commit=$(add_msg "$dir" $headrev "$rev" |
|
||||||
|
git commit-tree "$tree" $headp -p "$revp") || exit $?
|
||||||
|
fi
|
||||||
|
git reset "$commit" || exit $?
|
||||||
|
|
||||||
|
say "Added dir '$dir'"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_split () {
|
||||||
|
debug "Splitting $dir..."
|
||||||
|
cache_setup || exit $?
|
||||||
|
|
||||||
|
if test -n "$onto"
|
||||||
|
then
|
||||||
|
debug "Reading history for --onto=$onto..."
|
||||||
|
git rev-list $onto |
|
||||||
|
while read rev
|
||||||
|
do
|
||||||
|
# the 'onto' history is already just the subdir, so
|
||||||
|
# any parent we find there can be used verbatim
|
||||||
|
debug " cache: $rev"
|
||||||
|
cache_set "$rev" "$rev"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -n "$ignore_joins"
|
||||||
|
then
|
||||||
|
unrevs=
|
||||||
|
else
|
||||||
|
unrevs="$(find_existing_splits "$dir" "$revs")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# We can't restrict rev-list to only $dir here, because some of our
|
||||||
|
# parents have the $dir contents the root, and those won't match.
|
||||||
|
# (and rev-list --follow doesn't seem to solve this)
|
||||||
|
grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
|
||||||
|
revmax=$(eval "$grl" | wc -l)
|
||||||
|
revcount=0
|
||||||
|
createcount=0
|
||||||
|
eval "$grl" |
|
||||||
|
while read rev parents
|
||||||
|
do
|
||||||
|
revcount=$(($revcount + 1))
|
||||||
|
progress "$revcount/$revmax ($createcount)"
|
||||||
|
debug "Processing commit: $rev"
|
||||||
|
exists=$(cache_get "$rev")
|
||||||
|
if test -n "$exists"
|
||||||
|
then
|
||||||
|
debug " prior: $exists"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
createcount=$(($createcount + 1))
|
||||||
|
debug " parents: $parents"
|
||||||
|
newparents=$(cache_get $parents)
|
||||||
|
debug " newparents: $newparents"
|
||||||
|
|
||||||
|
tree=$(subtree_for_commit "$rev" "$dir")
|
||||||
|
debug " tree is: $tree"
|
||||||
|
|
||||||
|
check_parents $parents
|
||||||
|
|
||||||
|
# ugly. is there no better way to tell if this is a subtree
|
||||||
|
# vs. a mainline commit? Does it matter?
|
||||||
|
if test -z "$tree"
|
||||||
|
then
|
||||||
|
set_notree "$rev"
|
||||||
|
if test -n "$newparents"
|
||||||
|
then
|
||||||
|
cache_set "$rev" "$rev"
|
||||||
|
fi
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
|
||||||
|
debug " newrev is: $newrev"
|
||||||
|
cache_set "$rev" "$newrev"
|
||||||
|
cache_set latest_new "$newrev"
|
||||||
|
cache_set latest_old "$rev"
|
||||||
|
done || exit $?
|
||||||
|
|
||||||
|
latest_new=$(cache_get latest_new)
|
||||||
|
if test -z "$latest_new"
|
||||||
|
then
|
||||||
|
die "No new revisions were found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -n "$rejoin"
|
||||||
|
then
|
||||||
|
debug "Merging split branch into HEAD..."
|
||||||
|
latest_old=$(cache_get latest_old)
|
||||||
|
git merge -s ours \
|
||||||
|
--allow-unrelated-histories \
|
||||||
|
-m "$(rejoin_msg "$dir" "$latest_old" "$latest_new")" \
|
||||||
|
"$latest_new" >&2 || exit $?
|
||||||
|
fi
|
||||||
|
if test -n "$branch"
|
||||||
|
then
|
||||||
|
if rev_exists "refs/heads/$branch"
|
||||||
|
then
|
||||||
|
if ! rev_is_descendant_of_branch "$latest_new" "$branch"
|
||||||
|
then
|
||||||
|
die "Branch '$branch' is not an ancestor of commit '$latest_new'."
|
||||||
|
fi
|
||||||
|
action='Updated'
|
||||||
|
else
|
||||||
|
action='Created'
|
||||||
|
fi
|
||||||
|
git update-ref -m 'subtree split' \
|
||||||
|
"refs/heads/$branch" "$latest_new" || exit $?
|
||||||
|
say "$action branch '$branch'"
|
||||||
|
fi
|
||||||
|
echo "$latest_new"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_merge () {
|
||||||
|
revs=$(git rev-parse $default --revs-only "$@") || exit $?
|
||||||
|
ensure_clean
|
||||||
|
|
||||||
|
set -- $revs
|
||||||
|
if test $# -ne 1
|
||||||
|
then
|
||||||
|
die "You must provide exactly one revision. Got: '$revs'"
|
||||||
|
fi
|
||||||
|
rev="$1"
|
||||||
|
|
||||||
|
if test -n "$squash"
|
||||||
|
then
|
||||||
|
first_split="$(find_latest_squash "$dir")"
|
||||||
|
if test -z "$first_split"
|
||||||
|
then
|
||||||
|
die "Can't squash-merge: '$dir' was never added."
|
||||||
|
fi
|
||||||
|
set $first_split
|
||||||
|
old=$1
|
||||||
|
sub=$2
|
||||||
|
if test "$sub" = "$rev"
|
||||||
|
then
|
||||||
|
say "Subtree is already at commit $rev."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
|
||||||
|
debug "New squash commit: $new"
|
||||||
|
rev="$new"
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=$(git version)
|
||||||
|
if test "$version" \< "git version 1.7"
|
||||||
|
then
|
||||||
|
if test -n "$message"
|
||||||
|
then
|
||||||
|
git merge -s subtree --message="$message" "$rev"
|
||||||
|
else
|
||||||
|
git merge -s subtree "$rev"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if test -n "$message"
|
||||||
|
then
|
||||||
|
git merge -Xsubtree="$prefix" \
|
||||||
|
--message="$message" "$rev"
|
||||||
|
else
|
||||||
|
git merge -Xsubtree="$prefix" $rev
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_pull () {
|
||||||
|
if test $# -ne 2
|
||||||
|
then
|
||||||
|
die "You must provide <repository> <ref>"
|
||||||
|
fi
|
||||||
|
ensure_clean
|
||||||
|
ensure_valid_ref_format "$2"
|
||||||
|
git fetch "$@" || exit $?
|
||||||
|
revs=FETCH_HEAD
|
||||||
|
set -- $revs
|
||||||
|
cmd_merge "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_push () {
|
||||||
|
if test $# -ne 2
|
||||||
|
then
|
||||||
|
die "You must provide <repository> <ref>"
|
||||||
|
fi
|
||||||
|
ensure_valid_ref_format "$2"
|
||||||
|
if test -e "$dir"
|
||||||
|
then
|
||||||
|
repository=$1
|
||||||
|
refspec=$2
|
||||||
|
echo "git push using: " "$repository" "$refspec"
|
||||||
|
localrev=$(git subtree split --prefix="$prefix") || die
|
||||||
|
git push "$repository" "$localrev":"refs/heads/$refspec"
|
||||||
|
else
|
||||||
|
die "'$dir' must already exist. Try 'git subtree add'."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
"cmd_$command" "$@"
|
||||||
Loading…
Reference in New Issue
Block a user