Skip to content

Instantly share code, notes, and snippets.

@drmingdrmer
Last active November 25, 2020 05:49

Revisions

  1. drmingdrmer revised this gist Nov 25, 2020. 1 changed file with 1 addition and 0 deletions.
    1 change: 1 addition & 0 deletions git-split
    Original file line number Diff line number Diff line change
    @@ -743,6 +743,7 @@ while getopts "b:m:p:s:orh" opname; do
    exit 0
    ;;
    [?])
    echo "unknown option: ($opname)"
    usage
    exit 1
    ;;
  2. drmingdrmer revised this gist Mar 27, 2019. 1 changed file with 66 additions and 29 deletions.
    95 changes: 66 additions & 29 deletions git-split
    Original file line number Diff line number Diff line change
    @@ -71,8 +71,11 @@ Options:
    -r Remove <fns> from original history.
    Without -r, the original history will not be modified.
    -m Merge back the new branch to commmit "<to>".
    -m works only when -r is specified.
    -m Merge back action:
    "" for doing nothing;
    "merge": merge the new branch to commmit "<to>";
    "rebase": rebase "<to>" onto the new branch.
    non empty -m implies -r.
    -s <prefix> Strip <prefix> dir segments from paths that are splitted out.
    E.g.:
    @@ -712,18 +715,19 @@ fn_match()
    }

    new_branch=tmpbranch
    merge=0
    merge=""
    remove=0
    orphan=0
    to_strip=0

    while getopts "b:p:s:morh" opname; do
    while getopts "b:m:p:s:orh" opname; do
    case $opname in
    b)
    new_branch=$OPTARG
    ;;
    m)
    merge=1
    merge=$OPTARG
    remove=1
    ;;
    o)
    orphan=1
    @@ -811,7 +815,7 @@ extract()
    commit=$(git_copy_commit "$c" "$tree" "-p $parent")
    dd "new commit:" $commit

    git reset --hard $commit
    git reset --hard $commit --

    parent=$commit
    done
    @@ -829,7 +833,7 @@ build_new_branch()
    git commit --allow-empty -m "init-root" || die commit init-root
    else
    info reset $new_branch to $base
    git reset --hard $base
    git reset --hard $base --
    fi

    git clean -dxf || die clean -dxf
    @@ -871,50 +875,83 @@ discard_changes()
    info "== END =="
    }

    merge_them()
    merge_or_rebase()
    {
    info "== merge splitted branch back to $tip ==" \
    && \
    if [ ".$new_branch" = "tmpbranch" ]; then
    git merge --commit --no-edit --no-ff $new_hash
    else
    git merge --commit --no-edit --no-ff $new_branch
    fi \
    && merged_hash=$(git_hash) \
    && info "merged hash: $merged_hash"
    local action=$1

    if [ "$action" == "merge" ]; then
    info "== merge splitted branch back to $tip ==" \
    && \
    if [ ".$new_branch" = "tmpbranch" ]; then
    git merge --commit --no-edit --no-ff $new_hash
    else
    git merge --commit --no-edit --no-ff $new_branch
    fi \
    && merged_hash=$(git_hash HEAD) \
    && info "merged hash: $merged_hash"

    elif [ "$action" == "rebase" ]; then

    info "== rebase $tip onto $new_branch ($new_hash) =="

    git rebase $new_hash || return 1
    merged_hash=$(git_hash HEAD)

    info "rebased hash: $merged_hash"
    else
    die "invalid action: ($action)"
    fi
    }

    is_same_as_original()
    {
    info "== diff with original $tip ==" \
    && git diff $tip
    info "== diff with original $tip =="
    git diff $tip || return 1
    ok "Current HEAD is same as $tip"
    }

    update_tip_ref()
    {
    info "remove tmp branch: refs/original/HEAD"
    git update-ref -d refs/original/HEAD

    # if it is a reference(branch, tag), not hash or HEAD

    if [ ".$tip" = ".HEAD" ]; then
    info "git update-ref $tip $merged_hash"
    git update-ref $tip $merged_hash \
    && info "== $tip set to $merged_hash"
    fi

    if git rev-parse refs/heads/$tip >/dev/null 2>/dev/null; then
    info "git update-ref refs/heads/$tip $merged_hash"
    git update-ref refs/heads/$tip $merged_hash \
    && info "== $tip set to $merged_hash"
    fi

    }

    info "split $fns from $range" \
    && build_new_branch \
    && { [ "$remove" = "0" ] && exit 0 || ``; } \
    && discard_changes \
    && { [ "$merge" = "0" ] && exit 0 || ``; } \
    && merge_them \
    && is_same_as_original \
    && update_tip_ref \
    && git checkout $tip >/dev/null 2>/dev/null \
    && info "== Done =="

    info "split $fns from $range"

    build_new_branch || die build_new_branch

    if [ "$remove" = "0" ]; then
    exit 0;
    fi

    discard_changes || die discard_changes

    if [ "$merge" = "" ]; then
    exit 0
    fi

    merge_or_rebase "$merge" || die merge_or_rebase

    is_same_as_original || die is_same_as_original

    update_tip_ref || die update_tip_ref

    git checkout $tip >/dev/null 2>/dev/null || die git checkout $tip

    info "== Done =="
  3. drmingdrmer revised this gist Mar 25, 2019. 1 changed file with 31 additions and 0 deletions.
    31 changes: 31 additions & 0 deletions git-split
    Original file line number Diff line number Diff line change
    @@ -84,6 +84,36 @@ END



    clr()
    {
    # clr light read blabla

    local black=0
    local white=7
    local red=1
    local green=2
    local brown=3
    local blue=4
    local purple=5
    local cyan=6

    local light=""
    local color=$1
    shift

    if [ "$color" == "light" ]; then
    light="tput bold; "
    color=$1
    shift
    fi

    local code=$(eval 'echo $'$color)
    local cmd="${light}tput setaf $code"
    local color_str="$(eval "$cmd")"

    echo $color_str"$@""$(tput sgr0)"
    }

    shlib_init_colors()
    {
    Black="$( tput setaf 0)"
    @@ -113,6 +143,7 @@ shlib_init_colors()
    NC="$( tput sgr0)" # No Color
    }


    screen_width()
    {
    local chr="${1--}"
  4. drmingdrmer revised this gist Feb 12, 2019. 1 changed file with 82 additions and 83 deletions.
    165 changes: 82 additions & 83 deletions git-split
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,87 @@
    #!/bin/bash

    usage()
    {
    cat <<-END
    Split one git history into two.
    One of them only contains changes of specified files.
    The other contains other changes(or optionally contains full history).
    Usage:
    git-split [ -b <to-new-branch> ] [-r] [-m] [-s <prefix> ] <from>..[<to>] -- <fns>
    Synoposis:
    Initial:
    * 358aee4 (HEAD) fix css
    | css/site.css | 13 ++---
    |
    * f869f40 init site
    | html/index.html | 12 ++--
    | html/header.html | 34 +-----
    | css/site.css | 61 +++++++------
    |
    * 82554be fix doc of Build
    To split folder html into a separate branch:
    git-split -m 82554be..HEAD -- html/
    Results in:
    * 948149a Merge commit 'aa76f4de5be5ddbb2908ba145fea5697c7baf728' into HEAD
    |\\
    | * aa76f4d init site
    | | html/index.html | 12 ++--
    | | html/header.html | 34 +-----
    | |
    * | 088e903 fix css
    | | css/site.css | 13 ++---
    | |
    * | 0b606c2 init site
    | | css/site.css | 61 +++++++------
    |/
    * 82554be fix doc of Build
    Arguments:
    <from> and <to> specifies history range to split.
    E.g. to split the latest 3 commit:
    git-split master~3..master
    "--" must present as a separator.
    <fns> are several file patterns.
    E.g.:
    git-split master~3..master -- src/foo.* deps/config.yaml
    Options:
    -b <new-branch> Create a new branch.
    When absent, create a temporary branch "tmpbranch".
    -o Create an orphan branch instead of one based on <from>.
    -r Remove <fns> from original history.
    Without -r, the original history will not be modified.
    -m Merge back the new branch to commmit "<to>".
    -m works only when -r is specified.
    -s <prefix> Strip <prefix> dir segments from paths that are splitted out.
    E.g.:
    git-split -s 3 base..master -- my/foo/dir/bar.txt
    removes "my/foo/dir".
    END
    }



    shlib_init_colors()
    @@ -598,89 +680,6 @@ fn_match()
    return 1
    }

    usage()
    {
    cat <<-END
    Split one git history into two.
    One of them only contains changes of specified files.
    The other contains other changes(or optionally contains full history).
    Usage:
    git-split [ -b <to-new-branch> ] [-r] [-m] [-s <prefix> ] <from>..[<to>] -- <fns>
    Synoposis:
    Initial:
    * 358aee4 (HEAD) fix css
    | css/site.css | 13 ++---
    |
    * f869f40 init site
    | html/index.html | 12 ++--
    | html/header.html | 34 +-----
    | css/site.css | 61 +++++++------
    |
    * 82554be fix doc of Build
    To split folder html into a separate branch:
    git-split -m 82554be..HEAD -- html/
    Results in:
    * 948149a Merge commit 'aa76f4de5be5ddbb2908ba145fea5697c7baf728' into HEAD
    |\\
    | * aa76f4d init site
    | | html/index.html | 12 ++--
    | | html/header.html | 34 +-----
    | |
    * | 088e903 fix css
    | | css/site.css | 13 ++---
    | |
    * | 0b606c2 init site
    | | css/site.css | 61 +++++++------
    |/
    * 82554be fix doc of Build
    Arguments:
    <from> and <to> specifies history range to split.
    E.g. to split the latest 3 commit:
    git-split master~3..master
    "--" must present as a separator.
    <fns> are several file patterns.
    E.g.:
    git-split master~3..master -- src/foo.* deps/config.yaml
    Options:
    -b <new-branch> Create a new branch.
    When absent, create a temporary branch "tmpbranch".
    -o Create an orphan branch instead of one based on <from>.
    -r Remove <fns> from original history.
    Without -r, the original history will not be modified.
    -m Merge back the new branch to commmit "<to>".
    -m works only when -r is specified.
    -s <prefix> Strip <prefix> dir segments from paths that are splitted out.
    E.g.:
    git-split -s 3 base..master -- my/foo/dir/bar.txt
    removes "my/foo/dir".
    END

    }

    new_branch=tmpbranch
    merge=0
    remove=0
  5. drmingdrmer revised this gist Feb 12, 2019. 1 changed file with 0 additions and 20 deletions.
    20 changes: 0 additions & 20 deletions git-split
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,5 @@
    #!/bin/bash

    #include 'shlib.sh' begin
    #!/bin/sh


    shlib_init_colors()
    @@ -530,24 +528,7 @@ git_diff_ln_new()
    }'
    }

    # test:

    # # file to root
    # git reset --hard HEAD
    # git_object_add_by_commit_path a HEAD dist/shlib.sh
    # git status
    # # dir to root
    # git reset --hard HEAD
    # git_object_add_by_commit_path a HEAD dist
    # git status
    # # file to folder
    # git reset --hard HEAD
    # git_object_add_by_commit_path a/b/c HEAD dist/shlib.sh
    # git status
    # # dir to folder
    # git reset --hard HEAD
    # git_object_add_by_commit_path a/b/c HEAD dist
    # git status


    os_detect()
    @@ -616,7 +597,6 @@ fn_match()
    esac
    return 1
    }
    #include 'shlib.sh' end

    usage()
    {
  6. drmingdrmer created this gist Feb 12, 2019.
    910 changes: 910 additions & 0 deletions git-split
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,910 @@
    #!/bin/bash

    #include 'shlib.sh' begin
    #!/bin/sh


    shlib_init_colors()
    {
    Black="$( tput setaf 0)"
    BlackBG="$( tput setab 0)"
    DarkGrey="$( tput bold; tput setaf 0)"
    LightGrey="$( tput setaf 7)"
    LightGreyBG="$( tput setab 7)"
    White="$( tput bold; tput setaf 7)"
    Red="$( tput setaf 1)"
    RedBG="$( tput setab 1)"
    LightRed="$( tput bold; tput setaf 1)"
    Green="$( tput setaf 2)"
    GreenBG="$( tput setab 2)"
    LightGreen="$( tput bold; tput setaf 2)"
    Brown="$( tput setaf 3)"
    BrownBG="$( tput setab 3)"
    Yellow="$( tput bold; tput setaf 3)"
    Blue="$( tput setaf 4)"
    BlueBG="$( tput setab 4)"
    LightBlue="$( tput bold; tput setaf 4)"
    Purple="$( tput setaf 5)"
    PurpleBG="$( tput setab 5)"
    Pink="$( tput bold; tput setaf 5)"
    Cyan="$( tput setaf 6)"
    CyanBG="$( tput setab 6)"
    LightCyan="$( tput bold; tput setaf 6)"
    NC="$( tput sgr0)" # No Color
    }

    screen_width()
    {
    local chr="${1--}"
    chr="${chr:0:1}"

    local width=$(tput cols 2||echo 80)
    width="${COLUMNS:-$width}"

    echo $width
    }

    hr()
    {
    # generate a full screen width horizontal ruler
    local width=$(screen_width)

    printf -vl "%${width}s\n" && echo ${l// /$chr};
    }

    remove_color()
    {
    # remove color control chars from stdin or first argument

    local sed=gsed
    which -s $sed || sed=sed

    local s="$1"
    if [ -z "$s" ]; then
    $sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g"
    else
    echo "$s" | remove_color
    fi

    }

    text_hr()
    {
    # generate a full screen width sperator line with text.
    # text_hr "-" "a title"
    # > a title -----------------------------------------
    #
    # variable LR=l|m|r controls alignment

    local chr="$1"
    shift

    local bb="$(echo "$@" | remove_color)"
    local text_len=${#bb}

    local width=$(screen_width)
    let width=width-text_len

    local lr=${LR-m}
    case $lr in
    m)
    let left=width/2
    let right=width-left
    echo "$(printf -vl "%${left}s\n" && echo ${l// /$chr})$@$(printf -vl "%${right}s\n" && echo ${l// /$chr})"
    ;;
    r)

    echo "$(printf -vl "%${width}s\n" && echo ${l// /$chr})$@"
    ;;
    *)
    # l by default
    echo "$@$(printf -vl "%${width}s\n" && echo ${l// /$chr})"
    ;;
    esac

    }



    SHLIB_LOG_VERBOSE=1
    SHLIB_LOG_FORMAT='[$(date +"%Y-%m-%d %H:%M:%S")] $level $title $mes'

    die()
    {
    err "$@" >&2
    exit 1
    }
    die_empty()
    {
    if test -z "$1"
    then
    shift
    die empty: "$@"
    fi
    }

    set_verbose()
    {
    SHLIB_LOG_VERBOSE=${1-1}
    }

    log()
    {
    local color="$1"
    local title="$2"
    local level="$_LOG_LEVEL"
    shift
    shift

    local mes="$@"
    local NC="$(tput sgr0)"

    if [ -t 1 ]; then
    title="${color}${title}${NC}"
    level="${color}${level}${NC}"
    fi
    eval "echo \"$SHLIB_LOG_FORMAT\""
    }
    dd()
    {
    debug "$@"
    }
    debug()
    {
    if [ ".$SHLIB_LOG_VERBOSE" = ".1" ]; then
    local LightCyan="$(tput bold ; tput setaf 6)"
    _LOG_LEVEL=DEBUG log "$LightCyan" "$@" >&2
    fi
    }
    info()
    {
    local Brown="$(tput setaf 3)"
    _LOG_LEVEL=" INFO" log "$Brown" "$@"
    }
    ok() {
    local Green="$(tput setaf 2)"
    _LOG_LEVEL=" OK" log "${Green}" "$@"
    }
    err() {
    local Red="$(tput setaf 1)"
    _LOG_LEVEL="ERROR" log "${Red}" "$@"
    }

    git_hash()
    {
    git rev-parse $1 \
    || die "'git_hash $@'"
    }
    git_is_merge()
    {
    test $(git cat-file -p "$1" | grep "^parent " | wc -l) -gt 1
    }
    git_parents()
    {
    git rev-list --parents -n 1 ${1-HEAD} | { read self parents; echo $parents; }
    }
    git_rev_list()
    {
    # --parents
    # print parent in this form:
    # <commit> <parent-1> <parent-2> ..

    git rev-list \
    --reverse \
    --topo-order \
    --default HEAD \
    --simplify-merges \
    "$@" \
    || die "'git rev-list $@'"
    }
    git_tree_hash()
    {
    git rev-parse "$1^{tree}"
    }
    git_ver()
    {
    local git_version=$(git --version | awk '{print $NF}')
    local git_version_1=${git_version%%.*}
    local git_version_2=${git_version#*.}
    git_version_2=${git_version_2%%.*}

    printf "%03d%03d" $git_version_1 $git_version_2
    }
    git_working_root()
    {
    git rev-parse --show-toplevel
    }

    git_rev_exist()
    {
    git rev-parse --verify --quiet "$1" >/dev/null
    }

    git_branch_default_remote()
    {
    local branchname=$1
    git config --get branch.${branchname}.remote
    }
    git_branch_default_upstream_ref()
    {
    local branchname=$1
    git config --get branch.${branchname}.merge
    }
    git_branch_default_upstream()
    {
    git rev-parse --abbrev-ref --symbolic-full-name "$1"@{upstream}

    # OR
    # git_branch_default_upstream_ref "$@" | sed 's/^refs\/heads\///'
    }
    git_branch_exist()
    {
    git_rev_exist "refs/heads/$1"
    }

    git_head_branch()
    {
    git symbolic-ref --short HEAD
    }

    git_commit_date()
    {

    # git_commit_date author|commit <ref> [date-format]

    # by default output author-date
    local what_date="%ad"
    if [ "$1" = "commit" ]; then
    # commit date instead of author date
    what_date="%cd"
    fi
    shift

    local ref=$1
    shift

    local fmt="%Y-%m-%d %H:%M:%S"
    if [ "$#" -gt 0 ]; then
    fmt="$1"
    fi
    shift

    git log -n1 --format="$what_date" --date=format:"$fmt" "$ref"
    }
    git_commit_copy()
    {
    # We're going to set some environment vars here, so
    # do it in a subshell to get rid of them safely later
    dd copy_commit "{$1}" "{$2}" "{$3}"
    git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%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

    # (echo -n "$annotate"; cat ) |

    git commit-tree "$2" $3 # reads the rest of stdin
    ) || die "Can't copy commit $1"
    }

    git_object_type()
    {
    # $0 ref|hash
    # output "commit", "tree" etc
    git cat-file -t "$@" 2>/dev/null
    }
    git_object_add_by_commit_path()
    {
    # add an blob or tree object to target_path in index
    # the object to add is specified by commit and path
    local target_path="$1"
    local src_commit="$2"
    local src_path="$3"

    local src_dir="$(dirname "$src_path")/"
    local src_name="$(basename "$src_path")"
    local src_treeish="$(git rev-parse "$src_commit:$src_dir")"

    git_object_add_by_tree_name "$target_path" "$src_treeish" "$src_name"

    }
    git_object_add_by_tree_name()
    {
    # add an blob or tree object to target_path in index
    local target_path="$1"
    local src_treeish="$2"
    local src_name="$3"

    dd "arg: target_path: ($target_path) src_treeish: ($src_treeish) src_name: ($src_name)"

    local target_dir="$(dirname $target_path)/"
    local target_fn="$(basename $target_path)"
    local treeish

    if [ -z "$src_name" ] || [ "$src_name" = "." ] || [ "$src_name" = "./" ]; then
    treeish="$src_treeish"
    else
    treeish=$(git ls-tree "$src_treeish" "$src_name" | awk '{print $3}')

    if [ -z "$treeish" ]; then
    die "source treeish not found: in tree: ($src_treeish) name: ($src_name)"
    fi
    fi

    dd "hash of object to add is: $treeish"

    if [ "$(git_object_type $treeish)" = "blob" ]; then
    # the treeish imported is a file, not a dir
    # first create a wrapper tree or replace its containing tree

    dd "object to add is blob"

    local dir_treeish
    local target_dir_treeish="$(git rev-parse "HEAD:$target_dir")"
    if [ -n "target_dir_treeish" ]; then
    dir_treeish="$(git rev-parse "HEAD:$target_dir")"
    dd "target dir presents: $target_dir"

    else
    dd "target dir absent"
    dir_treeish=""
    fi

    treeish=$(git_tree_add_blob "$dir_treeish" "$target_fn" $src_treeish $src_name) || die create wrapper treeish
    target_path="$target_dir"

    dd "wrapper treeish: $treeish"
    dd "target_path set to: $target_path"
    else
    dd "object to add is tree"
    fi

    git_treeish_add_to_prefix "$target_path" "$treeish"
    }

    git_treeish_add_to_prefix()
    {
    local target_path="$1"
    local treeish="$2"

    dd treeish content:
    git ls-tree $treeish

    git rm "$target_path" -r --cached || dd removing target "$target_path"

    if [ "$target_path" = "./" ]; then
    git read-tree "$treeish" \
    || die "read-tree $target_path $treeish"
    else
    git read-tree --prefix="$target_path" "$treeish" \
    || die "read-tree $target_path $treeish"
    fi
    }

    git_tree_add_tree()
    {
    # output new tree hash in stdout
    # treeish can be empty
    local treeish="$1"
    local target_fn="$2"
    local item_hash="$3"
    local item_name="$4"

    {
    if [ -n "$treeish" ]; then
    git ls-tree "$treeish" \
    | fgrep -v " $item_name"
    fi

    cat "040000 tree $item_hash $target_fn"
    } | git mktree
    }
    git_tree_add_blob()
    {
    # output new tree hash in stdout
    # treeish can be empty
    local treeish="$1"
    local target_fn="$2"
    local blob_treeish="$3"
    local blob_name="$4"

    {
    if [ -n "$treeish" ]; then
    git ls-tree "$treeish" \
    | fgrep -v " $target_fn"
    fi

    git ls-tree "$blob_treeish" "$blob_name" \
    | awk -v target_fn="$target_fn" -F" " '{print $1" "target_fn}'
    } | git mktree
    }

    git_workdir_save()
    {
    local index_hash=$(git write-tree)

    # add modified file to index and read index tree
    git add -u
    local working_hash=$(git write-tree)

    # restore index tree
    git read-tree $index_hash

    echo $index_hash $working_hash
    }
    git_workdir_load()
    {
    local index_hash=$1
    local working_hash=$2

    git_object_type $index_hash || die "invalid index hash: $index_hash"
    git_object_type $working_hash || die "invalid workdir hash: $working_hash"

    # First create a temp commit to restore working tree.
    #
    # git-read-index to index and git-reset does not work because deleted file in
    # index does not apply to working tree.
    #
    # But there is an issue with this:
    # git checkout --orphan br
    # git_workdir_load
    # would fails, because ORIG_HEAD is not a commit.

    local working_commit=$(echo "x" | git commit-tree $working_hash) || die get working commit
    git reset --hard $working_commit || die reset to tmp commit
    git reset --soft ORIG_HEAD || die reset to ORIG_HEAD
    git read-tree $index_hash || die "load saved index tree from $index_hash"
    }
    git_workdir_is_clean()
    {
    local untracked="$1"
    if [ "$untracked" == "untracked" ]; then
    [ -z "$(git status --porcelain)" ]
    else
    [ -z "$(git status --porcelain --untracked-files=no)" ]
    fi
    }

    git_copy_commit()
    {
    git_commit_copy "$@"
    }

    git_diff_ln_new()
    {
    # output changed line number of a file: <from> <end>; inclusive:
    # 27 28
    # 307 309
    # 350 350
    #
    # Usage:
    #
    # diff working tree with HEAD:
    # git_diff_ln_new HEAD -- <fn>
    #
    # diff working tree with staged:
    # git_diff_ln_new -- <fn>
    #
    # diff staged(cached) with HEAD:
    # git_diff_ln_new --cached -- <fn>
    #
    # in git-diff output:
    # for add lines:
    # @@ -53 +72,8
    #
    # for remove lines:
    # @@ -155 +179,0

    git diff -U0 "$@" \
    | grep '^@@' \
    | awk '{
    # @@ -155 +179,0
    # $1 $2 $3
    l = $3
    gsub("^+", "", l)
    # add default offset: ",1"
    split(l",1", x, ",")
    # inclusive line range:
    x[2] = x[1] + x[2] - 1
    # line remove format: @@ -155, +179,0
    # do need to output line range for removed.
    if (x[2] >= x[1]) {
    print x[1] " " x[2]
    }
    }'
    }

    # test:

    # # file to root
    # git reset --hard HEAD
    # git_object_add_by_commit_path a HEAD dist/shlib.sh
    # git status
    # # dir to root
    # git reset --hard HEAD
    # git_object_add_by_commit_path a HEAD dist
    # git status
    # # file to folder
    # git reset --hard HEAD
    # git_object_add_by_commit_path a/b/c HEAD dist/shlib.sh
    # git status
    # # dir to folder
    # git reset --hard HEAD
    # git_object_add_by_commit_path a/b/c HEAD dist
    # git status


    os_detect()
    {
    local os
    case $(uname -s) in
    Linux)
    os=linux ;;
    *[bB][sS][dD])
    os=bsd ;;
    Darwin)
    os=mac ;;
    *)
    os=unix ;;
    esac
    echo $os
    }

    mac_ac_power_connection()
    {
    # Connected: (Yes|No)
    system_profiler SPPowerDataType \
    | sed '1,/^ *AC Charger Information:/d' \
    | grep Connected:
    }


    mac_power()
    {

    # $0 is-battery exit code 0 if using battery.
    # $0 is-ac-power exit code 0 if using ac power.

    local cmd="$1"
    local os=$(os_detect)

    if [ "$os" != "mac" ]; then
    err "not mac but: $os"
    return 1
    fi

    case $cmd in

    is-battery)
    mac_ac_power_connection | grep -q No
    ;;

    is-ac-power)
    mac_ac_power_connection | grep -q Yes
    ;;

    *)
    err "invalid cmd: $cmd"
    return 1
    ;;
    esac
    }

    fn_match()
    {
    # $0 a.txt *.txt
    case "$1" in
    $2)
    return 0
    ;;
    esac
    return 1
    }
    #include 'shlib.sh' end

    usage()
    {
    cat <<-END
    Split one git history into two.
    One of them only contains changes of specified files.
    The other contains other changes(or optionally contains full history).
    Usage:
    git-split [ -b <to-new-branch> ] [-r] [-m] [-s <prefix> ] <from>..[<to>] -- <fns>
    Synoposis:
    Initial:
    * 358aee4 (HEAD) fix css
    | css/site.css | 13 ++---
    |
    * f869f40 init site
    | html/index.html | 12 ++--
    | html/header.html | 34 +-----
    | css/site.css | 61 +++++++------
    |
    * 82554be fix doc of Build
    To split folder html into a separate branch:
    git-split -m 82554be..HEAD -- html/
    Results in:
    * 948149a Merge commit 'aa76f4de5be5ddbb2908ba145fea5697c7baf728' into HEAD
    |\\
    | * aa76f4d init site
    | | html/index.html | 12 ++--
    | | html/header.html | 34 +-----
    | |
    * | 088e903 fix css
    | | css/site.css | 13 ++---
    | |
    * | 0b606c2 init site
    | | css/site.css | 61 +++++++------
    |/
    * 82554be fix doc of Build
    Arguments:
    <from> and <to> specifies history range to split.
    E.g. to split the latest 3 commit:
    git-split master~3..master
    "--" must present as a separator.
    <fns> are several file patterns.
    E.g.:
    git-split master~3..master -- src/foo.* deps/config.yaml
    Options:
    -b <new-branch> Create a new branch.
    When absent, create a temporary branch "tmpbranch".
    -o Create an orphan branch instead of one based on <from>.
    -r Remove <fns> from original history.
    Without -r, the original history will not be modified.
    -m Merge back the new branch to commmit "<to>".
    -m works only when -r is specified.
    -s <prefix> Strip <prefix> dir segments from paths that are splitted out.
    E.g.:
    git-split -s 3 base..master -- my/foo/dir/bar.txt
    removes "my/foo/dir".
    END

    }

    new_branch=tmpbranch
    merge=0
    remove=0
    orphan=0
    to_strip=0

    while getopts "b:p:s:morh" opname; do
    case $opname in
    b)
    new_branch=$OPTARG
    ;;
    m)
    merge=1
    ;;
    o)
    orphan=1
    ;;
    r)
    remove=1
    ;;
    s)
    to_strip="$OPTARG"
    ;;
    h)
    usage
    exit 0
    ;;
    [?])
    usage
    exit 1
    ;;
    esac
    done
    shift $(($OPTIND - 1))

    if [ ".$2" != ".--" ]; then
    usage
    exit 1
    fi

    range=$1
    shift
    shift
    fns="$@"

    base=${range%..*}
    if [ ".$base" = ".$range" ]; then
    usage
    exit 1
    fi

    tip=${range#*..}
    if [ ".$tip" = "." ]; then
    tip=HEAD
    fi

    head=$(git symbolic-ref --short HEAD 2>/dev/null)
    if [ ".$head" = "." ]; then
    head=$(git rev-parse HEAD)
    fi

    tip_hash=$(git_hash $tip)
    new_hash=
    merged_hash=


    extract()
    {
    local parent=$1
    local tree=
    local commit=

    for c in $(git log --reverse $base..$tip_hash --format="%H" -- $fns); do

    info "== to pick $fns from:"
    git --no-pager log --color --stat --decorate --pretty=oneline --abbrev-commit -M -n1 $c
    info "== END =="

    # mes=$(git log -n1 --format="%B" $c)

    # force prefix when diff, since .gitconfig could hide prefix
    git diff --src-prefix="" --dst-prefix="" --binary $c~ $c -- $fns | git apply -p $to_strip --index

    info "== after patch $c =="
    git status --short
    info "== END =="

    # manually commit index

    tree=$(git write-tree)

    dd "tree to commit: $tree"
    git ls-tree -r $tree
    dd "end of tree"

    dd parent: $parent

    commit=$(git_copy_commit "$c" "$tree" "-p $parent")
    dd "new commit:" $commit

    git reset --hard $commit

    parent=$commit
    done
    return 0
    }

    build_new_branch()
    {
    info "== build_new_branch =="

    git checkout --orphan $new_branch || die checkout --orpahn $new_branch

    if [ "$orphan" = "1" ]; then
    git reset --hard
    git commit --allow-empty -m "init-root" || die commit init-root
    else
    info reset $new_branch to $base
    git reset --hard $base
    fi

    git clean -dxf || die clean -dxf

    info "HEAD hash: $(git_hash HEAD)"

    extract $(git_hash HEAD) \
    && new_hash=$(git_hash HEAD) \
    && \
    if [ "$new_branch" = "tmpbranch" ]; then
    git checkout --detach HEAD \
    && git branch -d tmpbranch
    else
    :
    fi \
    && info "== new branch $new_branch hash is $new_hash == "
    }

    discard_changes()
    {
    info "== discard_changes =="

    # git checkout would fail if one of the files does not exists.
    # thus newly added file impedes the whole process.
    # git checkout ab12345 -- nonexistent

    # local script='for f in '"$fns"'; do git rm -r -f $f; git checkout '"$base"' -- $f; done'
    local script='for f in '"$fns"'; do echo "== $f =="; git rm -r -f $f; git status -- $f >&2; git checkout '"$base"' -- $f; git status -- $f >&2; done'

    git checkout --detach $tip_hash \
    && info "== discard from $tip ==" \
    && git filter-branch -f --prune-empty --tree-filter "$script" $base..HEAD \
    && [ $(git log $base..HEAD -- "$fns" | wc -l | awk '{print $1}') = 0 ] \
    && info "== confirmed that no changes through $base..HEAD"
    git status --short
    info "== END =="
    info "== history =="
    git --no-pager log --color --stat --decorate --pretty=oneline --abbrev-commit -M $base..HEAD
    info "== END =="
    }

    merge_them()
    {
    info "== merge splitted branch back to $tip ==" \
    && \
    if [ ".$new_branch" = "tmpbranch" ]; then
    git merge --commit --no-edit --no-ff $new_hash
    else
    git merge --commit --no-edit --no-ff $new_branch
    fi \
    && merged_hash=$(git_hash) \
    && info "merged hash: $merged_hash"
    }

    is_same_as_original()
    {
    info "== diff with original $tip ==" \
    && git diff $tip
    }

    update_tip_ref()
    {
    git update-ref -d refs/original/HEAD

    # if it is a reference(branch, tag), not hash or HEAD

    if [ ".$tip" = ".HEAD" ]; then
    git update-ref $tip $merged_hash \
    && info "== $tip set to $merged_hash"
    fi

    if git rev-parse refs/heads/$tip >/dev/null 2>/dev/null; then
    git update-ref refs/heads/$tip $merged_hash \
    && info "== $tip set to $merged_hash"
    fi

    }

    info "split $fns from $range" \
    && build_new_branch \
    && { [ "$remove" = "0" ] && exit 0 || ``; } \
    && discard_changes \
    && { [ "$merge" = "0" ] && exit 0 || ``; } \
    && merge_them \
    && is_same_as_original \
    && update_tip_ref \
    && git checkout $tip >/dev/null 2>/dev/null \
    && info "== Done =="