├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── TODO.md ├── git-stree └── git-stree-completion.bash /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We welcome all contributions, especially in the following areas: 2 | 3 | * Bugfixes 4 | * Extended completion (especially zsh compatibility, and Cygwin/msysgit for Windows users) 5 | * Unit tests 6 | 7 | To contribute, just fork this repository on GitHub, write your stuff and send a pull request. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Christophe Porteneuve 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stree: a better Git subtree command 2 | 3 | **Subtrees** are a great way to share a single tree across multiple projects using it in their own codebases. For many use-cases, they are a vastly superior alternative to submodules. Alas, there is no built-in equivalent to `git submodule` to help you properly manage subtrees. 4 | 5 | ## :warning: THIS PROJECT IS DEPRECATED 6 | 7 | Maintenance of `git stree` has stopped in favor of another third-party project: **[git-subrepo](https://github.com/ingydotnet/git-subrepo#readme)**. It is actively maintained, much more robust, with more features, excellent test coverage and system integration, and more. It does cover all the actual use cases, and there's no point in plodding forward with git-stree now. 8 | 9 | Get there for all your subrepo management needs, you'll love it! :heart: 10 | 11 | *We'd like to take this opportunity to thank all the people who gave git-stree a spin, and contributed some of their time to feedback and possibly pull requests. But because this project is now deprecated and maintenance has stopped, pull requests won't be accepted anymore, and issues are closed. If you **really want to keep with it**, feel free to fork and make progress! Thanks a ton for using this so far.* 12 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Code: split command 2 | * Doc: README: Link to subtrees article and emphasize it! 3 | * Code: maintain a .gitstrees on every add 4 | * Code: git stree sync, so we dup/sync the config from .gitstrees 5 | * Docs: maintain full help listing as a manpage, too 6 | * Code: install scripts? Puts completion and man in the proper places, etc. 7 | * Code: Unit tests, based on Git's builtin test infrastructure 8 | -------------------------------------------------------------------------------- /git-stree: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Git STree -- A better Git subtree helper command. 4 | # 5 | # http://tdd.github.io/git-stree 6 | # http://https://medium.com/@porteneuve/mastering-git-subtrees-943d29a798ec 7 | # 8 | # Copyright (c) 2014 Christophe Porteneuve 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining 11 | # a copy of this software and associated documentation files (the 12 | # "Software"), to deal in the Software without restriction, including 13 | # without limitation the rights to use, copy, modify, merge, publish, 14 | # distribute, sublicense, and/or sell copies of the Software, and to 15 | # permit persons to whom the Software is furnished to do so, subject to 16 | # the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be 19 | # included in all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | [ -n "$STREE_DEBUG" ] && set -x 30 | 31 | # Env/context-related flags so we know what extra commands can be called upon. 32 | { [ "cygwin" = "$TERM" ] || { tty -s <&1 && [[ "$TERM"=~"color" ]]; }; } && is_tty=true || is_tty=false 33 | { echo "foo" | iconv -t 'ASCII' &> /dev/null; } && has_iconv=true || has_iconv=false 34 | { echo "foo" | tr A-Z a-z &> /dev/null; } && has_tr=true || has_tr=false 35 | 36 | # Color constants (VT100 ANSI codes) 37 | CYAN=36 38 | GRAY=37 39 | GREEN=32 40 | RED=31 41 | YELLOW=33 42 | 43 | # Symbols 44 | if [ "cygwin" = "$TERM" ]; then 45 | CHECK="√" 46 | CROSS="X" 47 | else 48 | CHECK="✔" 49 | CROSS="✖" 50 | fi 51 | 52 | # Grabbing CLI arguments and the main subcommand 53 | args=("$@") 54 | subcmd=${args[0]} 55 | if [ -n "$subcmd" -a "--help" == "${args[1]}" ]; then 56 | args[0]=help 57 | args[1]="$subcmd" 58 | subcmd="${args[0]}" 59 | fi 60 | 61 | # Command: `git stree add name -P prefix url [branch]` 62 | # 63 | # Defines a subtree and performs the initial injection in the working directory. 64 | # Does not create a commit out of it. Some configuration is added to retain 65 | # subtree information (e.g. prefix, url, branch and latest sync'd commit). 66 | function add_subtree 67 | { 68 | local name=$(require_name) 69 | require_arg 2 'Missing -P parameter' '-P' > /dev/null 70 | local prefix=$(require_arg 3 'Missing prefix') 71 | prefix=$(normalize_prefix "$prefix") 72 | local url=$(require_arg 4 'Missing URL') 73 | local branch=$(optional_arg 5 'master') 74 | 75 | local root_key=$(get_root_key "$name") 76 | local remote_name=$(get_remote_name "$name") 77 | if [ $(git config --local --get "remote.$remote_name.url") ]; then 78 | error false "A remote already exists for '$name' ($remote_name). Subtree already defined?" 79 | fi 80 | 81 | check_conflicting_strees "$prefix" 82 | 83 | ensure_attached_head 84 | ensure_no_stage 85 | 86 | { git remote add -t "$branch" "$remote_name" "$url" && 87 | git fetch --quiet "$remote_name" && 88 | git config --local "stree.$root_key.prefix" "$prefix" && 89 | git config --local "stree.$root_key.branch" "$branch" && 90 | git read-tree --prefix="$prefix" -u "$remote_name/$branch" && 91 | git commit -m "[STree] Added stree '$root_key' in $prefix" && 92 | git config --local "stree.$root_key.latest-sync" "$(git rev-parse --short HEAD)" && 93 | echo '' && 94 | yay "STree '$root_key' configured, 1st injection committed."; 95 | } || { 96 | echo '' && 97 | git remote rm "$remote_name" && 98 | git config --local --remove-section "stree.$root_key" && 99 | error false "STree '$root_key' could not be configured."; 100 | } 101 | } 102 | 103 | # Helper: determines whether a branch exists 104 | function branch_exists 105 | { 106 | [ "refs/heads/$1" == "$(git rev-parse --symbolic-full-name --verify --quiet "$1")" ] 107 | } 108 | 109 | # Helper: maintains detected stree conflict state in a file, to work across subshell boundaries. 110 | function has_conflicts 111 | { 112 | local sentinel="$(git rev-parse --git-dir)/STREE_CONFLICTS" 113 | case "$1" in 114 | yes) echo 'yes' > "$sentinel";; 115 | reset) rm -f "$sentinel" && false;; 116 | *) [ -f "$sentinel" ] && [ "yes" = "$(cat "$sentinel")" ];; 117 | esac 118 | } 119 | 120 | # Helper: checks that the passed path doesn't conflict with existing stree 121 | # definitions. If it does, lists them, and asks for confirmation. Refusal 122 | # stops the script. 123 | function check_conflicting_strees() 124 | { 125 | local path="$1" 126 | local list=$(get_subtree_list) 127 | has_conflicts reset 128 | 129 | echo "$list" | while read stree remoting prefix; do 130 | [ "$prefix" != "$path" ] && continue 131 | 132 | has_conflicts || message $YELLOW "Existing strees use that prefix already:" 133 | message $YELLOW " • $stree ($remoting)" 134 | has_conflicts yes 135 | done 136 | 137 | has_conflicts || return 138 | has_conflicts reset 139 | 140 | confirm N "Do you want to proceed and setup another stree with that same prefix?" 141 | } 142 | 143 | # Command: `git stree clear` 144 | function clear_subtrees 145 | { 146 | if [ -n "${args[1]}" ]; then 147 | error false "This is not the command you're looking for. 148 | 149 | git stree clear removes all subtrees defined for this repository. You 150 | specified a specific subtree (${args[1]}) on the command line, so you probably 151 | want: 152 | 153 | git stree rm ${args[1]}" 154 | fi 155 | 156 | if [ "$subcmd" == "forget" ]; then 157 | message $CYAN "git stree forget has been deprecated: use git stree clear instead." 158 | fi 159 | 160 | for name in $(get_subtree_list simple); do 161 | rm_subtree "$name" && discreet "• Removed subtree '$name'" 162 | done 163 | yay 'Successfully cleared all subtree definitions.' 164 | } 165 | 166 | # Helper: confirms with a Y/N question and repeat until a proper answer is given. 167 | # Arguments: color, default (Y or N), message 168 | function confirm 169 | { 170 | local question="Yn" 171 | local reply="$1" 172 | 173 | [ "N" = "$reply" ] && question="yN" 174 | 175 | while true; do 176 | $is_tty && echo -en "\033[0;${CYAN}m" 177 | read -r -n 1 -p "$2 [$question] " reply 178 | $is_tty && echo -en "\033[0m" 179 | 180 | reply=$(echo "$reply" | tr a-z A-Z) 181 | [ -z "$reply" ] && reply="$1" 182 | [ "Y" = "$reply" -o "N" = "$reply" ] && break 183 | echo '' 184 | done 185 | 186 | [ "N" = "$reply" ] && exit 42 187 | } 188 | 189 | # Helper: discreet info message. This will show up in gray on STDOUT. 190 | function discreet 191 | { 192 | message $GRAY "$@" 193 | } 194 | 195 | # Helper: makes sure we're not on a detached HEAD 196 | function ensure_attached_head 197 | { 198 | [ 'HEAD' != "$(git rev-parse --abbrev-ref --symbolic HEAD)" ] && return 199 | error false "You are apparently on a detached HEAD. This is not a good point to commit from. Checkout a branch." 200 | } 201 | 202 | # Helper: makes sure we're in a Git repo. Piggy-backs on `git config --local` 203 | # to determine that, instead of traversing the filesystem upwards looking for `.git`. 204 | function ensure_git_repo 205 | { 206 | cmd=$(git rev-parse --is-inside-work-tree 2> /dev/null) 207 | [ "true" == "$cmd" ] || error false "You do not appear to be in a Git repository" 208 | } 209 | 210 | # Helper: makes sure there is nothing in the stage. 211 | function ensure_no_stage 212 | { 213 | git diff --cached --quiet || error false "You have staged changes already. This should not get conflated with an upcoming STree commit. Finalize your commit first or unstage your stuff." 214 | } 215 | 216 | # Helper: checks that the stree seems defined already. 217 | function ensure_stree_defined 218 | { 219 | local name="$1" 220 | local remote_name=$(get_remote_name "$name") 221 | local root_key=$(get_root_key "$name") 222 | 223 | for key in "remote.$remote_name.url" "stree.$root_key.prefix" "stree.$root_key.branch"; do 224 | git config --local --get "$key" &> /dev/null || 225 | error false "STree '$root_key' does not seem (fully) defined: missing '$key' configuration." 226 | done 227 | } 228 | 229 | # Helper: error message. This will show up in red on STDERR, followed by usage info, 230 | # then exit the script with exit code 1. 231 | function error 232 | { 233 | show_usage=$1 234 | shift 235 | message $RED "︎$CROSS ""$@"$'\n' >&2 236 | $show_usage && usage 237 | kill -s ABRT $$ 238 | } 239 | 240 | # Helper: computes a backport branch name based on the passed CLI name. 241 | function get_branch_name 242 | { 243 | echo "stree-backports-$(get_root_key "$1")" 244 | } 245 | 246 | # Helper: computes a remote name based on the passed CLI name. 247 | function get_remote_name 248 | { 249 | echo "stree-$(get_root_key "$1")" 250 | } 251 | 252 | # Helper: computes a root config key based on the passed CLI name. 253 | function get_root_key 254 | { 255 | local result="$1" 256 | $has_iconv && result=$(echo "$result" | iconv -t 'ASCII//TRANSLIT//IGNORE') 257 | $has_tr && result=$(echo "$result" | tr A-Z a-z) 258 | result=$(echo "$result" | sed 's/[^a-z0-9_ -]\+//g' | sed -e 's/^ \+\| \+$//g' -e 's/ \+/-/g') 259 | [ -z "$result" ] && error false "STree name '$1' does not yield a usable remote name. Try using ASCII letters/numbers in it." 260 | echo "$result" 261 | } 262 | 263 | # Helper: returns a list of 3-tuples, one for each defined stree. Tuples are 264 | # three quoted strings: the stree’s name, its remoting (remote-name/remote-branch), 265 | # and its WD prefix subdirectory. 266 | function get_subtree_list 267 | { 268 | git config --local --get-regexp 'remote\.stree.*\.url' | sort | while read key url; do 269 | local name="$(sed 's/remote\.stree-\|\.url//g' <<< "$key")" 270 | if [ 'simple' == "$1" ]; then 271 | echo "$name" 272 | else 273 | local branch="$(git config --local "stree.$name.branch")" 274 | local prefix="$(git config --local "stree.$name.prefix")" 275 | 276 | printf "%q %q %q\n" "$name" "$url@$branch" "$prefix" 277 | fi 278 | done 279 | } 280 | 281 | # Command: `git stree list [-v]` 282 | function list_subtrees 283 | { 284 | local list=$(get_subtree_list) 285 | local verbose=false 286 | [ "-v" == "${args[1]}" ] && verbose=true 287 | 288 | echo "$list" | while read stree remoting prefix; do 289 | [ -z "$stree" ] && continue 290 | local branch_name=$(get_branch_name "$stree") 291 | local backports=false 292 | local backporting='' 293 | branch_exists "$branch_name" && backports=true 294 | $backports && backporting=" (backports through $branch_name)" 295 | 296 | echo "• $stree [$prefix] <=> $remoting$backporting" 297 | if $verbose; then 298 | local latest_sha=$(git config --local "stree.$stree.latest-sync") 299 | local infix='' 300 | $backports && infix=' ' 301 | echo '' 302 | git show -s --pretty=format:" %C(auto)Latest sync:$infix %h - %ad - %s (%an)%n" "$latest_sha" 303 | 304 | if $backports; then 305 | git show -s --pretty=format:' %C(auto)Latest backport: %h - %ad - %s (%an)%n' "$branch_name" 306 | fi 307 | echo '' 308 | fi 309 | done 310 | } 311 | 312 | # Helper: info/success message. This will show up in cyan on STDOUT. 313 | function meh 314 | { 315 | message $CYAN '✔︎ '"$@"$'\n' 316 | } 317 | 318 | # Helper: message. Takes a color code as first arg, then the message as remaining 319 | # args. Only injects VT100 ANSI codes if we're on a color-supporting TTY output 320 | # (which is detected using STDOUT, by the way, so YMMV when redirecting to STDERR). 321 | function message 322 | { 323 | local color="$1" 324 | shift 325 | $is_tty && echo -en "\033[0;${color}m" 326 | echo -n "$@" 327 | $is_tty && echo -e "\033[0m" || echo '' 328 | } 329 | 330 | # Helper: normalizes a cwd-relative prefix so it starts from the root of the working directory. 331 | function normalize_prefix 332 | { 333 | local root=$(dirname $(git rev-parse --git-dir)) 334 | 335 | if [ '.' == "$root" ]; then 336 | echo "$1" 337 | return 338 | fi 339 | 340 | local path="$(pwd -P)/$1" 341 | path="${path//\/.\//\/}" 342 | while [[ "$path"=~"([^/][^/]*/\.\./)" ]]; do 343 | path="${path/${BASH_REMATCH[0]}/}" 344 | done 345 | 346 | sed "s@$root/@@" <<< "$path" 347 | } 348 | 349 | # Helper: gets an argument from the CLI, if present, otherwise uses the default 350 | # passed as $2 351 | function optional_arg 352 | { 353 | local result=${args[$1]} 354 | [ -n "$result" ] && echo "$result" || echo "$2" 355 | } 356 | 357 | # Command: `git stree pull name` 358 | # 359 | # Pulls remote updates for a properly-configured subtree, and squash-merges them 360 | # as a single commit in the current branch. This requires a non-detached HEAD and 361 | # an empty stage, so we don't conflate our work with ongoing commit construction. 362 | function pull_subtree 363 | { 364 | local name=$(require_name) 365 | 366 | ensure_attached_head 367 | ensure_no_stage 368 | ensure_stree_defined "$name" 369 | 370 | local root_key=$(get_root_key "$name") 371 | local remote=$(get_remote_name "$name") 372 | local branch=$(git config --local "stree.$root_key.branch") 373 | local log_size=$(echo "${args[@]}" | sed -n 's/^.*--log=\([0-9]\+\).*$/\1/p') 374 | [ -z "$log_size" ] && log_size=20 375 | 376 | git fetch --quiet "$remote" && 377 | git merge --quiet -s subtree --squash --log=$log_size "$remote/$branch" &> /dev/null || exit $? 378 | 379 | echo '' 380 | 381 | if git diff --cached --quiet; then 382 | meh "STree '$root_key' pulled, but no updates found." 383 | else 384 | local latest_sync=$(git config --local "stree.$root_key.latest-sync") 385 | [ -n "$latest_sync" ] || latest_sync='(use all)' 386 | 387 | local msg_file="$(git rev-parse --git-dir)/SQUASH_MSG" 388 | local msg="[STree] Pulled stree '$root_key'"$'\n\n'"$(sed "/^commit $latest_sync/,100000d" "$msg_file")" 389 | echo "$msg" > "$msg_file" 390 | local commits=$(grep --count '^commit ' "$msg_file") 391 | git commit -F "$msg_file" && 392 | git config --local "stree.$root_key.latest-sync" "$(git rev-parse --short HEAD)" 393 | yay "STree '$root_key' pulled, $commits update(s) committed." 394 | fi 395 | } 396 | 397 | # Command: `git stree push name [commits...]` 398 | # 399 | # Pushes local commits for a properly-configured subtree on its upstream. 400 | # This can either take a series of specific commits, or will auto-determine 401 | # a list of commits to be used since the last sync. These commits are 402 | # cherry-picked on a special integration branch that first rebase-pulls 403 | # from upstream, then the new set is pushed back. 404 | function push_subtree 405 | { 406 | local name=$(require_name) 407 | 408 | ensure_no_stage 409 | ensure_stree_defined "$name" 410 | 411 | local root_key=$(get_root_key "$name") 412 | local prefix=$(git config --local "stree.$root_key.prefix") 413 | 414 | local -a commits=(${args[@]:2}) 415 | if [ ${#commits[@]} -eq 0 ]; then 416 | local latest=$(git config --local "stree.$root_key.latest-sync") 417 | if [ -z "$latest" ]; then 418 | error false "Cannot find the most recent sync point for this subtree :-(" 419 | fi 420 | 421 | latest=$(git rev-parse --short "$latest") 422 | local root_dir="$(dirname $(git rev-parse --git-dir))" 423 | cd "$root_dir" 424 | commits=($(git rev-list --reverse --abbrev-commit "$latest".. -- "$prefix")) 425 | cd - > /dev/null 426 | if [ ${#commits[@]} -eq 0 ]; then 427 | meh "No local commits found for subtree '$name' since latest sync ($latest)" 428 | return 429 | fi 430 | else 431 | local parsed_ref 432 | for ((i = 0; i < ${#commits[@]}; ++i)); do 433 | parsed_ref=$(git rev-parse --short "${commits[$i]}" 2> /dev/null) 434 | if [ 0 = $? ]; then 435 | commits[$i]=$parsed_ref 436 | else 437 | error false "Cannot resolve commit: ${commits[$i]}" 438 | fi 439 | done 440 | fi 441 | 442 | local latest_head=$(git rev-parse --symbolic --abbrev-ref HEAD) 443 | 444 | local remote_name=$(get_remote_name "$name") 445 | local branch=$(git config --local "stree.$root_key.branch") 446 | local branch_name=$(get_branch_name "$name") 447 | 448 | if branch_exists "$branch_name"; then 449 | git checkout --quiet --merge "$branch_name" && 450 | git fetch "$remote_name" && 451 | git rebase --preserve-merges --autostash --quiet "$remote_name/$branch" &> /dev/null 452 | else 453 | git checkout --quiet --track -b "$branch_name" "$remote_name/$branch" 454 | fi 455 | 456 | for commit in "${commits[@]}"; do 457 | git cherry-pick -x -X subtree="$prefix" "$commit" > /dev/null && 458 | git config --local "stree.$root_key.latest-sync" "$commit" && 459 | discreet "• $(git show -s --oneline "$commit")" || 460 | error false "Could not cherry-pick $(git show -s --oneline "$commit")" 461 | done 462 | 463 | git push --quiet "$remote_name" "$branch_name":"$branch" && 464 | git checkout --quiet --merge "$latest_head" && 465 | yay "STree '$name' successfully backported local changes to its remote" 466 | } 467 | 468 | # Helper: require that an argument still be available in the list provided on the CLI 469 | # and consume it, possibly verifying it is a given fixed string (then passed as $2). 470 | # 471 | # An error message *must* be provided as $1 should the argument be missing or incorrect. 472 | # In such a case, it's passed to `error`, thereby stopping the script. 473 | function require_arg 474 | { 475 | local result="${args[$1]}" 476 | [ -n "$3" -a "$3" != "$result" ] && result='' 477 | if [ "$result" ]; then 478 | echo "$result" 479 | return 480 | fi 481 | 482 | error true "$2" 483 | } 484 | 485 | # Helper: just a comfort wrapper over `require_arg` for the most common use case. 486 | function require_name 487 | { 488 | require_arg 1 'Missing subtree name' 489 | } 490 | 491 | # Command: `git stree rm name` 492 | # 493 | # Removes all definitions (configuration entries) and backport branch for the given 494 | # subtree, but leaves the subdirectory contents in place. 495 | function rm_subtree 496 | { 497 | local name="$1" 498 | [ -z "$name" ] && name=$(require_name) 499 | local root_key=$(get_root_key "$name") 500 | local remote_name=$(get_remote_name "$name") 501 | local branch_name=$(get_branch_name "$name") 502 | 503 | git config --local --remove-section "stree.$root_key" &> /dev/null 504 | git remote rm "$remote_name" &> /dev/null 505 | git branch -D "$branch_name" &> /dev/null 506 | [ -z "$1" ] && yay "All settings removed for STree '$root_key'." 507 | true 508 | } 509 | 510 | # Command: `git stree split name -P path url [branch]` 511 | # 512 | # Creates a proper subtree branch from a subdirectory's contents and history. 513 | # Then the subtree's backport branch is configured and pushed (to either `master` 514 | # or the specified branch). 515 | function split_subtree 516 | { 517 | local name=$(require_name) 518 | require_arg 2 'Missing -P parameter' '-P' > /dev/null 519 | local prefix=$(require_arg 3 'Missing prefix') 520 | prefix=$(normalize_prefix "$prefix") 521 | local url=$(require_arg 4 'Missing URL') 522 | local branch=$(optional_arg 5 'master') 523 | 524 | local root_key=$(get_root_key "$name") 525 | local remote_name=$(get_remote_name "$name") 526 | if [ $(git config --local --get "remote.$remote_name.url") ]; then 527 | error false "A remote already exists for '$name' ($remote_name). Subtree already defined?" 528 | fi 529 | 530 | ensure_attached_head 531 | ensure_no_stage 532 | 533 | local latest_head=$(git rev-parse --symbolic --abbrev-ref HEAD) 534 | local branch_name=$(get_branch_name "$name") 535 | 536 | if branch_exists "$branch_name"; then 537 | error false "A subtree backport branch already exists for '$name' ($branch_name). Subtree already defined/split?" 538 | fi 539 | 540 | git remote add -t "$branch" "$remote_name" "$url" && 541 | git config --local "stree.$root_key.prefix" "$prefix" && 542 | git config --local "stree.$root_key.branch" "$branch" && 543 | git checkout -b "$branch_name" --quiet && 544 | git filter-branch -f --subdirectory-filter "$prefix" > /dev/null && 545 | git push --quiet -u "$remote_name" "$branch_name":"$branch" > /dev/null && 546 | git config --local "stree.$root_key.latest-sync" "$(git rev-parse HEAD)" && 547 | git checkout --quiet --merge "$latest_head" && 548 | yay "STree '$root_key' configured, split and pushed." 549 | } 550 | 551 | # Helper: usage display on STDERR. Used when an error occurs or when the CLI 552 | # args don't start with a valid command. 553 | function usage 554 | { 555 | local cmd="$subcmd" 556 | 557 | if [ "help" == "$cmd" -a -n "${args[1]}" ]; then 558 | cmd="${args[1]}" 559 | elif [ "help" == "$cmd" ]; then 560 | cmd="" 561 | fi 562 | 563 | if ! [[ "@add@clear@forget@help@list@pull@push@rm@split@"=~"@$cmd@" ]]; then 564 | cmd="" 565 | fi 566 | 567 | if [ -z "$cmd" ]; then 568 | cat >&2 <<-EOT 569 | Usage: $0 sub-command [options...] 570 | 571 | Sub-commands: 572 | 573 | EOT 574 | else 575 | cat >&2 <<-EOT 576 | Usage: $0 $cmd [options…] 577 | 578 | EOT 579 | fi 580 | 581 | if [ -z "$cmd" -o "add" == "$cmd" ]; then 582 | cat >&2 <<-EOT 583 | add name -P prefix url [branch] 584 | 585 | Defines a new subtree and performs its initial fetch and prefixed 586 | (subdirectory) checkout. You can specify a custom branch to track, 587 | otherwise it will use \`master\`. This creates a few local configuration 588 | entries that will be needed later. 589 | 590 | EOT 591 | fi 592 | if [ -z "$cmd" -o "forget" == "$cmd" -o "clear" == "$cmd" ]; then 593 | cat >&2 <<-EOT 594 | clear (formerly "forget") 595 | 596 | "Forgets" all subtrees if no identifiers are passed. This essentially 597 | does \`git stree rm\` over each subtree in turn. 598 | 599 | EOT 600 | fi 601 | if [ -z "$cmd" -o "list" == "$cmd" ]; then 602 | cat >&2 <<-EOT 603 | list [-v] 604 | 605 | Lists all defined subtrees. If the \`-v\` option is set, displays their 606 | latest sync (central -> subtree) commit and latest backport (subtree -> central) 607 | with their timestamps. 608 | 609 | EOT 610 | fi 611 | if [ -z "$cmd" -o "pull" == "$cmd" ]; then 612 | cat >&2 <<-EOT 613 | pull name [--log=20] 614 | 615 | Attempts to pull remote updates for a subtree you already defined. 616 | This is a no-rebase, squash-commit update that will not create any 617 | extra line in your history graph, but result in a single update commit 618 | on your current branch. 619 | 620 | If you wish to change the maximum number of merged commits info in the 621 | resulting squash commit, use the --log= option. Defaults to 20. 622 | 623 | EOT 624 | fi 625 | if [ -z "$cmd" -o "push" == "$cmd" ]; then 626 | cat >&2 <<-EOT 627 | push name [commits...] 628 | 629 | Pushes your local work on the subtree to its defined remote. If you 630 | specify commits, only these will be cherry-picked. Otherwise, it will 631 | cherry-pick all commits related to the subtree that occurred since the 632 | latest \`add\`/\`pull\`. This creates/maintains a subtree-specific 633 | backport branch that you should not manually touch. 634 | 635 | EOT 636 | fi 637 | if [ -z "$cmd" -o "rm" == "$cmd" ]; then 638 | cat >&2 <<-EOT 639 | rm name 640 | 641 | Removes all definitions for the given subtree (but leaves the subdirectory 642 | contents in place). 643 | 644 | EOT 645 | fi 646 | if [ -z "$cmd" -o "split" == "$cmd" ]; then 647 | cat >&2 <<-EOT 648 | stree split name -P path url [branch] 649 | 650 | Creates a proper subtree branch from a subdirectory's contents and history. 651 | Then the subtree's backport branch is configured and pushed (to either \`master\` 652 | or the specified branch). 653 | 654 | EOT 655 | fi 656 | if [ -z "$cmd" -o "help" == "$cmd" ]; then 657 | cat >&2 <<-EOT 658 | help [command] 659 | 660 | Displays this usage information, or the command’s usage information. 661 | EOT 662 | fi 663 | } 664 | 665 | # Helper: success message. This will show up in green on STDOUT. 666 | function yay { 667 | message $GREEN "$CHECK ""$@"$'\n' 668 | } 669 | 670 | # Allow subshells (such as functions called within a `$(…)` subshell) 671 | # to exit the parent script (the main `git-stree` script) by sending it 672 | # an ABRT (6) signal. See `error` for the trigger side of this. 673 | function exit1 { 674 | exit 1 675 | } 676 | trap exit1 ABRT 677 | 678 | ## MAIN ENTRY POINT ## 679 | 680 | [[ "$subcmd"=~"he?l?p?" ]] || ensure_git_repo 681 | 682 | case "$subcmd" in 683 | a|ad|add) 684 | add_subtree;; 685 | c|cl|cle|clea|clear|f|fo|for|forg|forge|forget) 686 | clear_subtrees;; 687 | l|li|lis|list) 688 | list_subtrees;; 689 | pul|pull) 690 | pull_subtree;; 691 | pus|push) 692 | push_subtree;; 693 | r|rm) 694 | rm_subtree;; 695 | s|sp|spl|spli|split) 696 | split_subtree;; 697 | ""|*) 698 | usage;; 699 | esac 700 | -------------------------------------------------------------------------------- /git-stree-completion.bash: -------------------------------------------------------------------------------- 1 | # bash completion support for Git STree 2 | # 3 | # http://tdd.github.io/git-stree 4 | # 5 | # Copyright (c) 2014 Christophe Porteneuve 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining 8 | # a copy of this software and associated documentation files (the 9 | # "Software"), to deal in the Software without restriction, including 10 | # without limitation the rights to use, copy, modify, merge, publish, 11 | # distribute, sublicense, and/or sell copies of the Software, and to 12 | # permit persons to whom the Software is furnished to do so, subject to 13 | # the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | # Helper: produces a completion reply with a given suffix on every value 27 | function __git_stree_build_completion { 28 | COMPREPLY=() 29 | local i=0 word 30 | for word in $1; do 31 | COMPREPLY[i++]="$word$2" 32 | done 33 | } 34 | 35 | # The main completion builder. 36 | function __git_stree_complete { 37 | local index=$((COMP_CWORD - __GIT_STREE_COMPLETE_OFFSET)) 38 | local cmd=${COMP_WORDS[$((1 + __GIT_STREE_COMPLETE_OFFSET))]} 39 | 40 | # Subcommand 41 | if [ 1 -eq $index ] || [ 2 -eq $index -a "help" == "$cmd" ] ; then 42 | __git_stree_build_completion "$(compgen -W "add clear help list pull push rm split" "$2" )" ' ' 43 | return 44 | fi 45 | 46 | # These take no arguments 47 | if [ 'clear' == "$cmd" -o "help" == "$cmd" ]; then 48 | return 49 | fi 50 | 51 | if [ 2 -eq $index ]; then 52 | # add/split needs a new, unique name as second argument: can't complete on it 53 | if [ 'add' == "$cmd" -o 'split' == "$cmd" ]; then 54 | return 55 | fi 56 | 57 | if [ 'list' == "$cmd" ]; then 58 | COMPREPLY=(-v) 59 | return 60 | fi 61 | 62 | # Other commands expect a subtree name as second argument 63 | COMPREPLY=($(__git_stree_complete_list "$2")) 64 | return 65 | fi 66 | 67 | # 'pull' accepts 1 fixed extra argument: '--log=' 68 | if [ 'pull' == "$cmd" ] && [ 3 -eq $index ] && [ '--log=' != "${COMP_WORDS[3]}" ]; then 69 | COMPREPLY=("--log=") 70 | return 71 | fi 72 | 73 | # Besides 'pull', only 'add' / 'split' take more than 1 extra argument 74 | [ 'add' == "$cmd" -o 'split' == "$cmd" ] || return 75 | 76 | # 3: -P mandatory option 77 | if [ 3 -eq $index ]; then 78 | COMPREPLY=("-P ") 79 | # 4: Mandatory value for the -P option. Will be a directory (may not exist) 80 | elif [ 4 -eq $index -a '-P' == "$3" ]; then 81 | COMPREPLY=($(compgen -d -o nospace -S / "$2" | grep -v '^.git$')) 82 | # 5+: external URL and possible branch name; can't complete 83 | fi 84 | } 85 | 86 | # Helper. This is actually nearly a copy of git-stree's get_subtree_list helper function. 87 | function __git_stree_complete_list { 88 | git config --local --get-regexp "remote\.stree-$1.*\.url" | sort | while read key _; do 89 | sed 's/remote\.stree-\|\.url//g' <<< "$key" 90 | done 91 | } 92 | 93 | # Wrapper: completion entry point when used as git-stree (single command). 94 | function __git_stree_complete_main { 95 | __GIT_STREE_COMPLETE_OFFSET=0 96 | __git_stree_complete "$@" 97 | } 98 | 99 | # Wrapper: completion entry point when used as git stree (git subcommand). 100 | function __git_stree_main { 101 | if [ "stree" == "${COMP_WORDS[1]}" ]; then 102 | __GIT_STREE_COMPLETE_OFFSET=1 103 | __git_stree_complete "$@" 104 | elif declare -F __git_wrap__git_main > /dev/null; then 105 | __git_wrap__git_main "$@" 106 | fi 107 | } 108 | 109 | # Register completion functions (and re-register git completion so we can inject 110 | # stree subcommand completion). 111 | complete -o nospace -F __git_stree_complete_main git-stree 112 | complete -o bashdefault -o default -o nospace -F __git_stree_main git 2>/dev/null \ 113 | || complete -o default -o nospace -F __git_stree_main git 114 | --------------------------------------------------------------------------------