├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── test_gitmux.sh ├── gitmux.sh └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.14 2 | 3 | ARG GH_VERSION 4 | ENV GH_VERSION ${GH_VERSION:-1.13.1} 5 | WORKDIR /gitmux 6 | 7 | COPY gitmux.sh . 8 | 9 | # Install dependencies 10 | RUN apk update && \ 11 | apk upgrade && \ 12 | apk add --no-cache \ 13 | bash \ 14 | git \ 15 | openssh \ 16 | jq 17 | 18 | # Install the GitHub CLI 19 | RUN wget https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz && \ 20 | tar -xf gh_${GH_VERSION}_linux_amd64.tar.gz && \ 21 | ln -s /gitmux/gh_${GH_VERSION}_linux_amd64/bin/gh /usr/local/bin/gh && \ 22 | rm /gitmux/gh_${GH_VERSION}_linux_amd64.tar.gz 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | REPOSITORY ?= gitmux 3 | 4 | .PHONY: 5 | ls: 6 | @docker images --no-trunc --format '{{json .}}' | \ 7 | jq -r 'select((.Repository|contains("$(REPOSITORY)")))' | jq -rs 'sort_by(.Repository)|.[]|"\(.ID)\t\(.Repository):\(.Tag)\t(\(.CreatedSince))\t[\(.Size)]"' 8 | 9 | .PHONY: 10 | build: 11 | @docker build \ 12 | --tag samstav/$(REPOSITORY):latest \ 13 | --file Dockerfile . 14 | 15 | .PHONY: 16 | push: 17 | docker push samstav/gitmux:latest 18 | 19 | .PHONY: 20 | run: 21 | docker run \ 22 | --interactive \ 23 | --tty \ 24 | --rm \ 25 | --stop-timeout=60 \ 26 | --volume $(shell pwd)/gitmux.sh:/gitmux.sh \ 27 | --volume $(HOME)/.ssh:/root/.ssh \ 28 | samstav/$(REPOSITORY):latest \ 29 | /bin/bash 30 | 31 | .PHONY: 32 | run-test: 33 | docker run \ 34 | --env GH_HOST \ 35 | --env GH_TOKEN \ 36 | --env GITHUB_OWNER \ 37 | --interactive --tty \ 38 | --volume $(shell pwd)/gitmux.sh:/gitmux/gitmux.sh \ 39 | --volume $(shell pwd)/test_gitmux.sh:/gitmux/test_gitmux.sh \ 40 | samstav/$(REPOSITORY):latest \ 41 | /bin/bash -c \ 42 | "git config --global user.email \"$(shell git config --global user.email)\" && \ 43 | git config --global user.name \"$(shell git config --global user.name)\" && \ 44 | /gitmux/test_gitmux.sh" 45 | 46 | 47 | define cleanup = 48 | repositoriesToDelete=$(gh repo list --limit 99 --json nameWithOwner --json name --jq '.[]|select(.name|startswith("gitmux_test_")).nameWithOwner') 49 | for r in ${repositoriesToDelete}; do 50 | echo "Deleting ${r}" 51 | gh api --method DELETE repos/"${r}" 52 | done 53 | endef 54 | 55 | cleanup: ; $(value cleanup) 56 | 57 | .ONESHELL: 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # gitmux 3 | 4 | ### If you've ever thought 💭 "I wish this were a separate repo", you've come to the right place. 5 | 6 | The `gitmux.sh` script is provided to help sync changes (**including commit history**) _across_ repositories. 7 | _ 8 | The script can be used to create brand new git repositories from _any_ (_**or** all_) content within a chosen source git repository. It can also be used to update repositories previously "forked" by gitmux (or those git repositories forked in a more traditional manner). 9 | 10 | You could also think of it as a tool for forking repositories, but with a twist: _you don't have to fork the entire repository_. Only the pieces/files you want. 11 | 12 | For assistance, email me `hi@stav.xyz` or submit an issue here. 13 | 14 | ### Who is this for? 15 | 16 | * Someone who wants to "fork" a subset of a larger repository into a new repository 17 | * Someone who wants to **update** a "fork" of a subset of a larger repository 18 | * Someone who wants to turn their github gist into a repository 19 | * Someone who previously "forked" (or copy/pasted) part of a larger repository and wants to do-it-over so that they get the commit history and tags 20 | * Someone who is quite well versed in git, and wants to explore the differences in the available rebase strategies (see help on the `-X` flag ) 21 | * A robot that checks for updates of git repositories being mirrored, with the goal of submitting a pull-request to the downstream mirror with any updates available 22 | 23 | ### Usage Notes 24 | 25 | * The recommended usage includes `-s`, which submits a pull request to your target repository with the resulting content. Although this is recommended, it is not the default, since it requires [`gh`](https://cli.github.com/) to be installed. 26 | 27 | * The pull request mechanism allows for discrete modifications to be made in both the source and destination repositories. In other words, the sync performed by this script is one-way which _should_ allow for additional changes in both your source repository and destination repository over time. 28 | 29 | * This script can be run many times for the same source and destination. For example, if you run this script for the first time on a Monday, and the **_source_** repository is updated on Wednesday, simply run this script again with the same arguments and it will generate a pull request with the latest updates from your _source_ repository. 30 | 31 | * If `-c` is used, the destination repository will be created if it does not yet exist. [Requires \`gh\` GitHub CLI.](https://cli.github.com/) 32 | 33 | * If `-s` is used, the pull request will be automatically submitted to your destination branch. [Requires \`gh\` GitHub CLI.](https://cli.github.com/) 34 | 35 | * The script does not push updates to `master`, only to `update-from-${GIT_BRANCH}-${GIT_SHA}` where `GIT_BRANCH` is the source repository branch referenced (defaults to HEAD/master) and `GIT_SHA` is the equivalent commit hash for that branch. For this reason, you don't need to worry about this script modifying any branches except for the custom "feature branch" it creates for its own use on your remote. 36 | 37 | * Changes make it into your destination repository's specified target branch ([default](https://help.github.com/en/articles/setting-the-default-branch) or `master` branch if not otherwise specified) through an auditable pull-request mechanism, and **are not** pushed to that branch directly by gitmux. If `-s` is not used or `gh` is not installed, you will need to merge the resulting changes from the gitmux feature branch into your destination branch manually. 38 | 39 | * The _new_, or destination/target repository must have at least one commit (cannot be an empty repository) if you provide the repository path/url instead of having gitmux create it for you. 40 | 41 | ### gitmux FAQ 42 | 43 | **1) Why doesnt this script push to my destination branch automatically?** 44 | 45 | That's dangerous. The best mechanism to view proposed changes is a 46 | Pull Request so that is the mechanism used by this script. A unique 47 | integration branch is created by this script in order to audit and 48 | view proposed changes and the result of the filtered source repository. 49 | 50 | **2) This script always clones my source repo, can I just point to a local 51 | directory containing a git repository as the source?** 52 | 53 | Yes. Feel free to use a local path for the source repository. That will 54 | make the syncing much faster, but to minimize the chance that you miss 55 | updates made in your source repository, supplying a URL is more consistent. 56 | 57 | **3) I want to manage the rebase myself in order to cherry-pick specific chanages. 58 | Is that possible?** 59 | 60 | Sure is. Just supply -i to the script and you will be given a \`cd\` 61 | command that will allow you to drop into the temporary workspace. 62 | From there, you can complete the interactive rebase and push your 63 | changes to the remote named 'destination'. The distinction between 64 | remote names in the workspace is very imporant. To double-check, use 65 | `git remote --verbose show` inside the gitmux git workspace. 66 | 67 | -------------------------------------------------------------------------------- /test_gitmux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Undefined variables are errors. 4 | set -euoE pipefail 5 | 6 | errcho () 7 | { 8 | printf "%s\n" "$@" 1>&2 9 | } 10 | 11 | errxit () 12 | { 13 | errcho "$@" 14 | cleanup 15 | exit 1 16 | } 17 | 18 | _pushd () { 19 | command pushd "$@" > /dev/null 20 | } 21 | 22 | _popd () { 23 | command popd > /dev/null 24 | } 25 | 26 | function log () { 27 | printf "%s\n" "$@" 28 | } 29 | 30 | _tree_func () { 31 | if [ -x "$(command -v tree)" ]; then 32 | tree 33 | return $? 34 | else 35 | find . -print | sort | sed 's;[^/]*/;|---;g;s;---|; |;g' 36 | return $? 37 | fi 38 | } 39 | 40 | 41 | 42 | # Constants / Arguments 43 | # To override, user should export $GH_HOST before running this test script. 44 | export GH_HOST=${GH_HOST:-'github.com'} 45 | export GITHUB_OWNER=${GITHUB_OWNER:-} 46 | 47 | TMPTESTWORKDIR=$(mktemp -t 'gitmux-test-XXXXXX' -d || errxit "Failed to create tmpdir.") 48 | echo "Working in tmpdir ${TMPTESTWORKDIR}" 49 | _pushd "${TMPTESTWORKDIR}" 50 | 51 | repositoriesToDelete=() 52 | cleanup() { 53 | errcho "Cleaning up!" 54 | rm -rf "${TMPTESTWORKDIR}" 55 | for r in "${repositoriesToDelete[@]}"; do 56 | echo "Deleting ${r}" 57 | gh api --method DELETE repos/"${r}" 58 | done 59 | echo "🛀" 60 | } 61 | 62 | # shellcheck disable=SC2120 63 | errcleanup() { 64 | if [ -n "${1:-}" ]; then 65 | _errmsg="⏩ Error at line ${1}" 66 | if [ -n "${2:-}" ]; then 67 | _errmsg="${_errmsg} in function '${2}'" 68 | fi 69 | errcho "${_errmsg}" 70 | fi 71 | errcho "⛔️ Tests failed." 72 | cleanup 73 | exit 1 74 | } 75 | 76 | trap 'errcleanup ${LINENO} ${FUNCNAME:-}' ERR 77 | 78 | rands() { 79 | # Usage: rands 80 | echo $RANDOM$RANDOM | tr '0-9' '[:lower:]' 81 | } 82 | 83 | REPO_REGEX='s/(.*:\/\/|^git@)(.*)([\/:]{1})([a-zA-Z0-9_\.-]{1,})([\/]{1})([a-zA-Z0-9_\.-]{1,}$)' 84 | 85 | 86 | createRepository() { 87 | local _owner="${1}" 88 | local _project="${2}" 89 | local _visibility=${3:-'public'} 90 | if [[ -z "${_project}" ]] || [[ -z "${_owner}" ]]; then 91 | errxit "Repository owner and project are required. Usage: \`createRepository \`" 92 | fi 93 | 94 | _ghcreateopts='' 95 | case ${_visibility} in 96 | internal) _ghcreateopts="--internal" ;; 97 | public) _ghcreateopts="--public" ;; 98 | private) _ghcreateopts="--private" ;; 99 | *) errxit "Not a valid value for visibility (choose one of public/private)";; 100 | esac 101 | 102 | ########## ################ 103 | # `gh repo create` must be run from inside a git repository. (weird) 104 | # gh repo create [] [flags] 105 | TMPGHCREATEWORKDIR=$(mktemp -t 'gitmux-tests-XXXXXX' -d || errxit "Failed to create tmpdir.") 106 | _pushd "${TMPGHCREATEWORKDIR}" 107 | NEW_REPOSITORY_DESCRIPTION="Test repository for gitmux. If you find this lingering you may safely delete this repository." 108 | log "gh-cli is creating your new repository now!" 109 | gh repo create "${_owner}/${_project}" ${_ghcreateopts:-} --license=unlicense --gitignore 'VVVV' --confirm --description "${NEW_REPOSITORY_DESCRIPTION}" 110 | pushd ${_project} 111 | log "renaming origin to hello" 112 | git remote rename origin hello 113 | pwd 114 | 115 | #_new_url=$(git remote get-url hello | sed -E "${REPO_REGEX}""/https\:\/\/${GH_TOKEN}\@\2\/\4\/\6/") 116 | _new_url=$(git remote get-url hello | sed -E "${REPO_REGEX}""/https\:\/\/git\:${GH_TOKEN}\@\2\/\4\/\6/") 117 | log "new url: ${_new_url}" 118 | git remote set-url hello ${_new_url} 119 | 120 | git commit --message 'Hello: this repository was created by gitmux.' --allow-empty 121 | git remote --verbose show 122 | log "pushing change to hello" 123 | git push hello "trunk:trunk" 124 | pwd 125 | _popd && _popd 126 | pwd 127 | log "cleaning up gh-create-repo workdir --> ${TMPGHCREATEWORKDIR}" 128 | rm -rf "${TMPGHCREATEWORKDIR}" 129 | ########## ################ 130 | } 131 | 132 | 133 | ##################################### 134 | #### Setup source git repository. 135 | ##################################### 136 | SOURCE_REPOSITORY_NAME="gitmux_test_source_$(rands)" 137 | mkdir -p "${SOURCE_REPOSITORY_NAME}" 138 | _pushd "${SOURCE_REPOSITORY_NAME}" && SOURCE_REPOSITORY_PATH="$(pwd)" 139 | git init --initial-branch=trunk 140 | createRepository "${GITHUB_OWNER}" "${SOURCE_REPOSITORY_NAME}" 141 | repositoriesToDelete+=("${GITHUB_OWNER}/${SOURCE_REPOSITORY_NAME}") 142 | git remote add source_remote_name "https://${GITHUB_OWNER}:${GH_TOKEN}@${GH_HOST}/${GITHUB_OWNER}/${SOURCE_REPOSITORY_NAME}.git" 143 | log "Fetching in $(pwd)" 144 | git fetch source_remote_name 145 | git checkout -b something-new --track source_remote_name/trunk 146 | echo "Hello World" > "hello.txt" 147 | echo "## wat" > 'wat.md' 148 | mkdir -p toto 149 | echo 'TUTU' > 'toto/tutu.txt' 150 | echo 'TATA' > 'toto/tata.txt' 151 | git add "hello.txt" 152 | git commit -m 'initial source repo commit: gitmux test' 153 | git add "wat.md" 154 | git commit -m 'and now wat?' 155 | git add toto 156 | git commit -m 'toto/ 🇫🇷' 157 | _sha=$(git rev-parse --short HEAD) 158 | _popd 159 | 160 | ##################################### 161 | #### Setup destination git repository. 162 | ##################################### 163 | DESTINATION_REPOSITORY_NAME="gitmux_test_destination_$(rands)" 164 | mkdir -p "${DESTINATION_REPOSITORY_NAME}" 165 | _pushd "${DESTINATION_REPOSITORY_NAME}" 166 | DESTINATION_REPOSITORY_PATH="$(pwd)" 167 | git init --initial-branch=trunk 168 | createRepository "${GITHUB_OWNER}" "${DESTINATION_REPOSITORY_NAME}" 169 | repositoriesToDelete+=("${GITHUB_OWNER}/${DESTINATION_REPOSITORY_NAME}") 170 | git remote add destination_remote_name "https://${GITHUB_OWNER}:${GH_TOKEN}@${GH_HOST}/${GITHUB_OWNER}/${DESTINATION_REPOSITORY_NAME}.git" 171 | git fetch --update-head-ok destination_remote_name 172 | # This actually creates a local 'trunk' tracking branch. 173 | git checkout trunk 174 | # Now back to current branch. 175 | git checkout -b destination_current_branch --track destination_remote_name/trunk 176 | git commit --allow-empty -m 'initial destination repo commit: gitmux test' 177 | _popd && _popd 178 | 179 | 180 | echo 181 | echo "*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*" 182 | echo 183 | 184 | ########################################## 185 | #### Test 1: 186 | #### - defaults 187 | #### - use existing github repository 188 | #### - rebase strategy 'ours' 189 | ########################################## 190 | 191 | test_defaults_with_existing_upstream_destination() { 192 | ./gitmux.sh -v -r "${SOURCE_REPOSITORY_PATH}" -t "${DESTINATION_REPOSITORY_PATH}" 193 | _pushd "${DESTINATION_REPOSITORY_PATH}" 194 | git checkout "update-from-something-new-${_sha}-rebase-strategy-ours" 195 | local output='' 196 | if output=$(cat hello.txt) && [ "${output}" == "Hello World" ];then 197 | echo "${output}" && echo "✅ Success" 198 | # reset 199 | git checkout destination_current_branch 200 | else 201 | errcleanup 202 | fi 203 | _popd 204 | } 205 | 206 | echo 207 | echo "*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*" 208 | echo 209 | 210 | ########################################## 211 | #### Test 2: 212 | #### - With -p (place in subdir at destination) 213 | #### - use existing github repository 214 | #### - rebase strategy 'theirs' 215 | ########################################## 216 | 217 | test_rebase_strategy_theirs_with_existing_upstream_destination() { 218 | ./gitmux.sh -v -r "${SOURCE_REPOSITORY_PATH}" -t "${DESTINATION_REPOSITORY_PATH}" -p place_content_in_this_subdir -b trunk -X theirs 219 | _pushd "${DESTINATION_REPOSITORY_PATH}" 220 | git checkout "update-from-something-new-${_sha}-rebase-strategy-theirs" 221 | local output='' 222 | if output=$(cat place_content_in_this_subdir/hello.txt) && [ "${output}" == "Hello World" ];then 223 | echo "${output}" && echo "✅ Success" 224 | else 225 | errcleanup 226 | fi 227 | _popd 228 | } 229 | 230 | echo 231 | echo "*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*" 232 | echo 233 | 234 | ########################################## 235 | #### Test 3: 236 | #### - defaults with -c (create repo for me) 237 | #### - gitmux should create repository for me 238 | #### - rebase strategy 'ours' 239 | ########################################## 240 | 241 | test_defaults_destination_dne_yet() { 242 | NEW_REPO_PROJECT_NAME="gitmux_test_destination_$(rands)" 243 | repositoriesToDelete+=("${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}") 244 | NEW_REPO_NO_UPSTREAM_YET="https://${GITHUB_OWNER}:${GH_TOKEN}@${GH_HOST}/${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}.git" 245 | ./gitmux.sh -v -c -r "${SOURCE_REPOSITORY_PATH}" -t "${NEW_REPO_NO_UPSTREAM_YET}" 246 | log "Now cloning repository which should have been created on GitHub by gitmux." 247 | git clone "${NEW_REPO_NO_UPSTREAM_YET}" 248 | # This should create a directory called $NEW_REPO_PROJECT_NAME 249 | _pushd "${NEW_REPO_PROJECT_NAME}" 250 | git checkout "update-from-something-new-${_sha}-rebase-strategy-ours" 251 | local output='' 252 | if output=$(cat hello.txt) && [ "${output}" == "Hello World" ];then 253 | echo "${output}" && echo "✅ Success" 254 | # reset 255 | git checkout destination_current_branch 256 | else 257 | errcleanup 258 | fi 259 | _popd 260 | } 261 | 262 | echo 263 | echo "*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*" 264 | echo 265 | 266 | ########################################## 267 | #### Test 4: 268 | #### - defaults with -c (create repo for me) 269 | #### - gitmux should create repository for me 270 | #### - rebase strategy 'ours' 271 | #### - add github team infraconfig/infracore 272 | ########################################## 273 | 274 | test_defaults_add_orgteam() { 275 | NEW_REPO_PROJECT_NAME="gitmux_test_destination_$(rands)" 276 | repositoriesToDelete+=("${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}") 277 | NEW_REPO_NO_UPSTREAM_YET="https://${GITHUB_OWNER}:${GH_TOKEN}@${GH_HOST}/${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}.git" 278 | ./gitmux.sh -v -c -r "${SOURCE_REPOSITORY_PATH}" -t "${NEW_REPO_NO_UPSTREAM_YET}" -z infraconfig/infracore 279 | log "Now cloning repository which should have been created on GitHub by gitmux." 280 | git clone "${NEW_REPO_NO_UPSTREAM_YET}" 281 | # This should create a directory called $NEW_REPO_PROJECT_NAME 282 | _pushd "${NEW_REPO_PROJECT_NAME}" 283 | # update-from-something-new-23eae47-rebase-strategy-ours 284 | git checkout "update-from-something-new-${_sha}-rebase-strategy-ours" 285 | local output='' 286 | if output=$(cat hello.txt) && [ "${output}" == "Hello World" ];then 287 | echo "${output}" && echo "✅ Success" 288 | # reset 289 | git checkout destination_current_branch 290 | else 291 | errcleanup 292 | fi 293 | _popd 294 | } 295 | 296 | echo 297 | echo "*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*" 298 | echo 299 | 300 | ########################################## 301 | #### Test 5: 302 | #### - defaults with -c (create repo for me) 303 | #### - gitmux should create repository for me 304 | #### - rebase strategy 'ours' 305 | #### - selective file migration 306 | ########################################## 307 | 308 | test_defaults_destination_dne_yet_only_wat() { 309 | NEW_REPO_PROJECT_NAME="gitmux_test_destination_$(rands)" 310 | repositoriesToDelete+=("${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}") 311 | NEW_REPO_NO_UPSTREAM_YET="https://${GITHUB_OWNER}:${GH_TOKEN}@${GH_HOST}/${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}.git" 312 | ./gitmux.sh -v -c -r "${SOURCE_REPOSITORY_PATH}" -t "${NEW_REPO_NO_UPSTREAM_YET}" -l "wat.md" 313 | log "Now cloning repository which should have been created on GitHub by gitmux." 314 | git clone "${NEW_REPO_NO_UPSTREAM_YET}" 315 | # This should create a directory called $NEW_REPO_PROJECT_NAME 316 | _pushd "${NEW_REPO_PROJECT_NAME}" 317 | git checkout "update-from-something-new-${_sha}-rebase-strategy-ours" 318 | if [ -f hello.txt ]; then 319 | errcho "File hello.txt should not be here" 320 | errcleanup 321 | fi 322 | local output='' 323 | pwd 324 | if output=$(cat wat.md) && [ "${output}" == "## wat" ];then 325 | echo "${output}" && echo "✅ Success" 326 | # reset 327 | git branches 328 | git checkout destination_current_branch 329 | else 330 | errcleanup 331 | fi 332 | _popd 333 | } 334 | 335 | test_defaults_destination_dne_yet_only_toto() { 336 | NEW_REPO_PROJECT_NAME="gitmux_test_destination_$(rands)" 337 | repositoriesToDelete+=("${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}") 338 | NEW_REPO_NO_UPSTREAM_YET="https://${GITHUB_OWNER}:${GH_TOKEN}@${GH_HOST}/${GITHUB_OWNER}/${NEW_REPO_PROJECT_NAME}.git" 339 | ./gitmux.sh -v -c -r "${SOURCE_REPOSITORY_PATH}" -t "${NEW_REPO_NO_UPSTREAM_YET}" -l "toto" 340 | log "Now cloning repository which should have been created on GitHub by gitmux." 341 | git clone "${NEW_REPO_NO_UPSTREAM_YET}" 342 | # This should create a directory called $NEW_REPO_PROJECT_NAME 343 | _pushd "${NEW_REPO_PROJECT_NAME}" 344 | git checkout "update-from-something-new-${_sha}-rebase-strategy-ours" 345 | if [ -f hello.txt ]; then 346 | errcho "File hello.txt should not be here" 347 | errcleanup 348 | fi 349 | if [ -f wat.md ]; then 350 | errcho "File wat.md should not be here" 351 | errcleanup 352 | fi 353 | local output='' 354 | pwd 355 | if output=$(cat toto/tutu.txt) && \ 356 | [ "${output}" == "TUTU" ] && \ 357 | output=$(cat toto/tata.txt) && \ 358 | [ "${output}" == "TATA" ] && \ 359 | _tree=$(_tree_func); then 360 | echo "${_tree}" && echo "✅ Success" 361 | # reset 362 | git branches 363 | git checkout destination_current_branch 364 | else 365 | errcleanup 366 | fi 367 | _popd 368 | } 369 | 370 | 371 | run_test_cases() { 372 | test_defaults_with_existing_upstream_destination 373 | test_rebase_strategy_theirs_with_existing_upstream_destination 374 | test_defaults_destination_dne_yet 375 | #test_defaults_add_orgteam 376 | test_defaults_destination_dne_yet_only_wat 377 | test_defaults_destination_dne_yet_only_toto 378 | } 379 | 380 | 381 | if run_test_cases; then 382 | echo '✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨' 383 | echo '✨ All tests completed successfully. ✨' 384 | echo '✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨' 385 | cleanup 386 | else 387 | errxit "Tests failed." 388 | fi 389 | -------------------------------------------------------------------------------- /gitmux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # See ./gitmux -h for more info. 4 | # 5 | # What does this script do? 6 | # This script creates a pull request on a destination repository 7 | # with content from a source repository and maintains all commit 8 | # history for all synced/forked files. 9 | # 10 | # See ./gitmux -h for more info. 11 | # 12 | # The pull request mechanism allows for discrete modifications 13 | # to be made in both the source and destination repositories. 14 | # The sync performed by this script is one-way which 15 | # _should_ allow for additional changes in both your source 16 | # repository and destination repository over time. 17 | # 18 | # This script can be run many times for the same source 19 | # and destination. If you run this script for the first time 20 | # on a Monday, and the source is updated on Wednesday, simply 21 | # run this script again and it will generate a pull request 22 | # with those updates which occurred in the interim. 23 | # 24 | # If -c is used, the destination repository will be created if it 25 | # does not yet exists. Requires \`gh\` GitHub CLI. 26 | # 27 | # https://cli.github.com 28 | # 29 | # If -s is used, the pull request will be automatically submitted 30 | # to your destination branch. Requires \`gh\` GitHub CLI. 31 | # 32 | # https://cli.github.com 33 | # 34 | # FAQ 35 | # 36 | # 1) Why doesnt this script push to my destination branch automatically? 37 | # 38 | # That's dangerous. The best mechanism to view proposed changes is a 39 | # Pull Request so that is the mechanism used by this script. A unique 40 | # integration branch is created by this script in order to audit and 41 | # view proposed changes and the result of the filtered source repository. 42 | # 43 | # 2) This script always clones my source repo, can I just point to a local 44 | # directory containing a git repository as the source? 45 | # 46 | # Yes. Feel free to use a local path for the source repository. That will 47 | # make the syncing much faster, but to minimize the chance that you miss 48 | # updates made in your source repository, supplying a URL is more consistent. 49 | # 50 | # 3) I want to manage the rebase myself in order to cherry-pick specific chanages. 51 | # Is that possible? 52 | # 53 | # Sure is. Just supply -i to the script and you will be given a \`cd\` 54 | # command that will allow you to drop into the temporary workspace. 55 | # From there, you can complete the interactive rebase and push your 56 | # changes to the remote named 'destination'. The distinction between 57 | # remote names in the workspace is very imporant. To double-check, use 58 | # `git remote --verbose show` inside the gitmux git workspace. 59 | 60 | # Undefined variables are errors. 61 | set -euoE pipefail 62 | 63 | errcho () 64 | { 65 | printf "%s\n" "$@" 1>&2 66 | } 67 | 68 | errxit () 69 | { 70 | errcho "$@" 71 | # shellcheck disable=SC2119 72 | errcleanup 73 | } 74 | 75 | _pushd () { 76 | command pushd "$@" > /dev/null 77 | } 78 | 79 | _popd () { 80 | command popd > /dev/null 81 | } 82 | 83 | _realpath () { 84 | if _cmd_exists realpath; then 85 | realpath $@ 86 | return $? 87 | else 88 | readlink -f $@ 89 | return $? 90 | fi 91 | } 92 | 93 | _cmd_exists () { 94 | if ! type "$*" &> /dev/null; then 95 | errcho "$* command not installed" 96 | return 1 97 | fi 98 | } 99 | 100 | cleanup() { 101 | if [[ -d ${gitmux_TMP_WORKSPACE:-} ]]; then 102 | # shellcheck disable=SC2086 103 | if [ ${KEEP_TMP_WORKSPACE:-false} = true ]; then 104 | # implement -k (keep) and check for it 105 | errcho "You may navigate to ${gitmux_TMP_WORKSPACE} to complete the workflow manually (or, try again)." 106 | else 107 | errcho "Cleaning up." 108 | rm -rf "${gitmux_TMP_WORKSPACE}" 109 | errcho "Deleted gitmux tmp workspace ${gitmux_TMP_WORKSPACE}" 110 | echo "🛀" 111 | fi 112 | fi 113 | } 114 | 115 | # shellcheck disable=SC2120 116 | errcleanup() { 117 | errcho "⛔️ gitmux execution failed." 118 | if [ -n "${1:-}" ]; then 119 | errcho "⏩ Error at line ${1}." 120 | fi 121 | cleanup 122 | exit 1 123 | } 124 | 125 | intcleanup() { 126 | errcho "🍿 Script discontinued." 127 | cleanup 128 | exit 1 129 | } 130 | 131 | trap 'errcleanup ${LINENO}' ERR 132 | trap 'intcleanup' SIGHUP SIGINT SIGTERM 133 | 134 | 135 | # Reset in case getopts has been used previously in the shell. 136 | OPTIND=1 137 | 138 | # Set defaults 139 | SOURCE_REPOSITORY="${SOURCE_REPOSITORY:-}" 140 | SUBDIRECTORY_FILTER="${SUBDIRECTORY_FILTER:-}" 141 | SOURCE_GIT_REF="${SOURCE_GIT_REF:-}" 142 | DESTINATION_PATH="${DESTINATION_PATH:-}" 143 | DESTINATION_REPOSITORY="${DESTINATION_REPOSITORY:-}" 144 | DESTINATION_BRANCH="${DESTINATION_BRANCH:-trunk}" 145 | SUBMIT_PR="${SUBMIT_PR:-false}" 146 | REV_LIST_FILES="${REV_LIST_FILES:-}" 147 | INTERACTIVE_REBASE="${INTERACTIVE_REBASE:-false}" 148 | CREATE_NEW_REPOSITORY="${CREATE_NEW_REPOSITORY:-false}" 149 | KEEP_TMP_WORKSPACE="${KEEP_TMP_WORKSPACE:-false}" 150 | 151 | # Don't default these rebase options *yet* 152 | MERGE_STRATEGY_OPTION_FOR_REBASE="${MERGE_STRATEGY_OPTION_FOR_REBASE:-ours}" 153 | REBASE_OPTIONS="${REBASE_OPTIONS:-}" 154 | GH_HOST="${GH_HOST:-github.com}" 155 | GITHUB_TEAMS=() 156 | 157 | source_repository="${SOURCE_REPOSITORY}" 158 | subdirectory_filter="${SUBDIRECTORY_FILTER}" 159 | source_git_ref="${SOURCE_GIT_REF}" 160 | destination_path="${DESTINATION_PATH}" 161 | destination_repository="${DESTINATION_REPOSITORY}" 162 | destination_branch="${DESTINATION_BRANCH}" 163 | rev_list_files="${REV_LIST_FILES}" 164 | _verbose=0 165 | 166 | function stripslashes () { 167 | echo "$@" | sed 's:/*$::' | sed 's:^/*::' 168 | } 169 | 170 | function log () { 171 | if [[ $_verbose -eq 1 ]]; then 172 | printf "%s\n" "$@" 173 | fi 174 | } 175 | 176 | function show_help() 177 | { 178 | # shellcheck disable=SC1111 179 | cat << EOF 180 | Usage: ${0##*/} [-r SOURCE_REPOSITORY] [-d SUBDIRECTORY_FILTER] [-g GITREF] [-t DESTINATION_REPOSITORY] [-p DESTINATION_PATH] [-b DESTINATION_BRANCH] [-X REBASE_STRATEGY_OPTION | -o REBASE_OPTIONS] [-z GITHUB_TEAM -z ...] [-i] [-s] [-c] [-k] [-v] [-h] 181 | “The life of a repo man is always intense.” 182 | -r Path/url to the [remote] source repository. Required. 183 | -t Path/url to the [remote] destination repository. Required. 184 | -d Directory within source repository to extract. This value is supplied to \`git filter-branch\` as --subdirectory-filter. (default: '/' which is effectively a fork of the entire repo.) Supply a value for -d to extract only a piece/subdirectory of your source repository. 185 | -g Git ref for the [remote] source repository. (default: null, which just uses the HEAD of the default branch, probably 'trunk (or master)', after cloning.) Can be any value valid for \`git checkout \` e.g. a branch, commit, or tag. 186 | -p Destination path for the filtered repository content ( default: '/' which places the repository content into the root of the destination repository. e.g. to place source repository's /app directory content into the /lib directory of your destination repository, supply -p lib ) 187 | -b Destination (a.k.a. base) branch in destination repository against which, changes will be rebased. Further, if [-s] is supplied, the resulting content will be submitted with this destination branch as the target (base) for the pull request. (Default: trunk) 188 | -l Options passed to git rev-list during \`git filter-branch\`. Can be used to specify individual files to be brought into the [new] repository. e.g. -l '--all -- file1.txt file2.txt' For more info see git's documentation for git filter-branch under the parameters for … 189 | -o Options to supply to \`git rebase\`. If set and includes --interactive or -i, this script will drop you into the workspace to complete the workflow manually (Note: cannot use with -X) 190 | -X