├── install.sh ├── LICENSE ├── README.md └── git-subsplit.sh /install.sh: -------------------------------------------------------------------------------- 1 | # Copy subsplit to where the Git scripts belong. 2 | cp git-subsplit.sh "$(git --exec-path)"/git-subsplit 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Dragonfly Development Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Git Subsplit 2 | ============ 3 | 4 | Automate and simplify the process of managing one-way read-only 5 | subtree splits. 6 | 7 | Git subsplit relies on `subtree` being available. If is not available 8 | in your version of git (likely true for versions older than 1.7.11) 9 | please install it manually from [here](https://github.com/apenwarr/git-subtree). 10 | 11 | 12 | Install 13 | ------- 14 | 15 | git-subsplit can be installed and run standalone by executing 16 | `git-subsplit.sh` directly. 17 | 18 | git-subsplit can also be installed as a git command by: 19 | 20 | ./install.sh 21 | 22 | Caveats 23 | ------- 24 | 25 | There is a known bug in the underlying git-subtree command that this script uses. Your disk will eventually run out of inodes because a cache directory isn't cleaned up after every run. I suggest you to create a cronjob to clean the cache directory every month: 26 | 27 | ``` 28 | 0 0 1 * * rm -rf /dflydev-git-subsplit-github-webhook/temp/$projectname/.subsplit/.git/subtree-cache/* 29 | ``` 30 | 31 | Hooks 32 | ----- 33 | 34 | ### GitHub WebHooks 35 | 36 | * [dflydev GitHub WebHook](https://github.com/dflydev/dflydev-git-subsplit-github-webhook) (**PHP**) 37 | 38 | 39 | Usage 40 | ----- 41 | 42 | ### Initialize 43 | 44 | Initialize subsplit with a git repository url: 45 | 46 | git subsplit init https://github.com/react-php/react 47 | 48 | This will create a working directory for the subsplit. It will contain 49 | a clone of the project's upstream repository. 50 | 51 | 52 | ### Update 53 | 54 | Update the subsplit repository with current state of its upstream 55 | repository: 56 | 57 | git subsplit update 58 | 59 | This command should be called before one or more `publish` commands 60 | are called to ensure that the repository in the working directory 61 | has been updated from its upstream repository. 62 | 63 | 64 | ### Publish 65 | 66 | Publish to each subtree split to its own repository: 67 | 68 | git subsplit publish \ 69 | src/React/EventLoop:git@github.com:react-php/event-loop.git \ 70 | --heads=master 71 | 72 | The pattern for the splits is `${subPath}:${url}`. Publish can receive 73 | its splits argument as a space separated list: 74 | 75 | git subsplit publish " 76 | src/React/EventLoop:git@github.com:react-php/event-loop.git 77 | src/React/Stream/:git@github.com:react-php/stream.git 78 | src/React/Socket/:git@github.com:react-php/socket.git 79 | src/React/Http/:git@github.com:react-php/http.git 80 | src/React/Espresso/:git@github.com:react-php/espresso.git 81 | " --heads=master 82 | 83 | This command will create subtree splits of the project's repository 84 | branches and tags. It will then push each branch and tag to the 85 | repository dedicated to the subtree. 86 | 87 | 88 | #### --update 89 | 90 | Passing `--update` to the `publish` command is a shortcut for calling 91 | the `update` command directly. 92 | 93 | 94 | #### --heads=\ 95 | 96 | To specify a list of heads (instead of letting git-subsplit discover them 97 | from the upstream repository) you can specify them directly. For example: 98 | 99 | --heads="master 2.0" 100 | 101 | The above will only sync the master and 2.0 branches, no matter which 102 | branches the upstream repository knows about. 103 | 104 | 105 | #### --no-heads 106 | 107 | Do not sync any heads. 108 | 109 | 110 | #### --tags=\ 111 | 112 | To specify a list of tags (instead of letting git-subsplit discover them 113 | from the upstream repository) you can specify them directly. For example: 114 | 115 | --tags="v1.0.0 v1.0.3" 116 | 117 | The above will only sync the v1.0.0 and v1.0.3 tags, no matter which 118 | tags the upstream repository knows about. 119 | 120 | 121 | #### --no-tags 122 | 123 | Do not sync any tags. 124 | 125 | 126 | #### --rebuild-tags 127 | 128 | Ordinarily tags will not be synced more than once. This is because in general 129 | tags should be considered more or less static. 130 | 131 | If for some reason tags need to be resynced from scratch (history changed so 132 | tags might point to somewhere else) this flag will get the job done. 133 | 134 | 135 | #### -q,--quiet 136 | 137 | As little output as possible. 138 | 139 | 140 | #### -n,--dry-run 141 | 142 | Does not actually publish information to the subsplit repos for each 143 | subtree split. Instead, display the command and execute the command 144 | with `--dry-run` included. 145 | 146 | #### --debug 147 | 148 | Allows you to see the logic behind the scenes. 149 | 150 | 151 | Not Invented Here 152 | ----------------- 153 | 154 | Inspiration for writing this came from [Guzzle's](http://guzzlephp.org/) 155 | goal of providing components as individually managed packages. Having 156 | seen this already done by [Symfony](http://symfony.com) and liking how 157 | it behaved I wanted to try and see if I could solve this problem in a 158 | general case so more people could take advantage of this workflow. 159 | 160 | Much time was spent checking out `git-subtree` and scripts written for 161 | managing [React's](http://nodephp.org/) components. 162 | 163 | 164 | License 165 | ------- 166 | 167 | MIT, see LICENSE. 168 | 169 | 170 | Community 171 | --------- 172 | 173 | If you have questions or want to help out, join us in the 174 | **#dflydev** channel on irc.freenode.net. 175 | -------------------------------------------------------------------------------- /git-subsplit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # git-subsplit.sh: Automate and simplify the process of managing one-way 4 | # read-only subtree splits. 5 | # 6 | # Copyright (C) 2012 Dragonfly Development Inc. 7 | # 8 | if [ $# -eq 0 ]; then 9 | set -- -h 10 | fi 11 | OPTS_SPEC="\ 12 | git subsplit init url 13 | git subsplit publish splits --heads= --tags= --splits= 14 | git subsplit update 15 | -- 16 | h,help show the help 17 | q quiet 18 | debug show plenty of debug output 19 | n,dry-run do everything except actually send the updates 20 | work-dir directory that contains the subsplit working directory 21 | 22 | options for 'publish' 23 | heads= only publish for listed heads instead of all heads 24 | no-heads do not publish any heads 25 | tags= only publish for listed tags instead of all tags 26 | no-tags do not publish any tags 27 | update fetch updates from repository before publishing 28 | rebuild-tags rebuild all tags (as opposed to skipping tags that are already synced) 29 | " 30 | eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)" 31 | 32 | # We can run this from anywhere. 33 | NONGIT_OK=1 34 | DEBUG=" :DEBUG >" 35 | 36 | PATH=$PATH:$(git --exec-path) 37 | 38 | . git-sh-setup 39 | 40 | if [ "$(hash git-subtree &>/dev/null && echo OK)" = "" ] 41 | then 42 | die "Git subplit needs git subtree; install git subtree or upgrade git to >=1.7.11" 43 | fi 44 | 45 | ANNOTATE= 46 | QUIET= 47 | COMMAND= 48 | SPLITS= 49 | REPO_URL= 50 | WORK_DIR="${PWD}/.subsplit" 51 | HEADS= 52 | NO_HEADS= 53 | TAGS= 54 | NO_TAGS= 55 | REBUILD_TAGS= 56 | DRY_RUN= 57 | VERBOSE= 58 | 59 | subsplit_main() 60 | { 61 | while [ $# -gt 0 ]; do 62 | opt="$1" 63 | shift 64 | case "$opt" in 65 | -q) QUIET=1 ;; 66 | --debug) VERBOSE=1 ;; 67 | --heads) HEADS="$1"; shift ;; 68 | --no-heads) NO_HEADS=1 ;; 69 | --tags) TAGS="$1"; shift ;; 70 | --no-tags) NO_TAGS=1 ;; 71 | --update) UPDATE=1 ;; 72 | -n) DRY_RUN="--dry-run" ;; 73 | --dry-run) DRY_RUN="--dry-run" ;; 74 | --rebuild-tags) REBUILD_TAGS=1 ;; 75 | --) break ;; 76 | *) die "Unexpected option: $opt" ;; 77 | esac 78 | done 79 | 80 | COMMAND="$1" 81 | shift 82 | 83 | case "$COMMAND" in 84 | init) 85 | if [ $# -lt 1 ]; then die "init command requires url to be passed as first argument"; fi 86 | REPO_URL="$1" 87 | shift 88 | subsplit_init 89 | ;; 90 | publish) 91 | if [ $# -lt 1 ]; then die "publish command requires splits to be passed as first argument"; fi 92 | SPLITS="$1" 93 | shift 94 | subsplit_publish 95 | ;; 96 | update) 97 | subsplit_update 98 | ;; 99 | *) die "Unknown command '$COMMAND'" ;; 100 | esac 101 | } 102 | say() 103 | { 104 | if [ -z "$QUIET" ]; 105 | then 106 | echo "$@" >&2 107 | fi 108 | } 109 | 110 | subsplit_require_work_dir() 111 | { 112 | if [ ! -e "$WORK_DIR" ] 113 | then 114 | die "Working directory not found at ${WORK_DIR}; please run init first" 115 | fi 116 | 117 | if [ -n "$VERBOSE" ]; 118 | then 119 | echo "${DEBUG} pushd \"${WORK_DIR}\" >/dev/null" 120 | fi 121 | 122 | pushd "$WORK_DIR" >/dev/null 123 | } 124 | 125 | subsplit_init() 126 | { 127 | if [ -e "$WORK_DIR" ] 128 | then 129 | die "Working directory already found at ${WORK_DIR}; please remove or run update" 130 | fi 131 | 132 | say "Initializing subsplit from origin (${REPO_URL})" 133 | 134 | if [ -n "$VERBOSE" ]; 135 | then 136 | echo "${DEBUG} git clone -q \"${REPO_URL}\" \"${WORK_DIR}\"" 137 | fi 138 | 139 | git clone -q "$REPO_URL" "$WORK_DIR" || die "Could not clone repository" 140 | } 141 | 142 | subsplit_publish() 143 | { 144 | subsplit_require_work_dir 145 | 146 | if [ -n "$UPDATE" ]; 147 | then 148 | subsplit_update 149 | fi 150 | 151 | if [ -z "$HEADS" ] && [ -z "$NO_HEADS" ] 152 | then 153 | # If heads are not specified and we want heads, discover them. 154 | HEADS="$(git ls-remote origin 2>/dev/null | grep "refs/heads/" | cut -f3- -d/)" 155 | 156 | if [ -n "$VERBOSE" ]; 157 | then 158 | echo "${DEBUG} HEADS=\"${HEADS}\"" 159 | fi 160 | fi 161 | 162 | if [ -z "$TAGS" ] && [ -z "$NO_TAGS" ] 163 | then 164 | # If tags are not specified and we want tags, discover them. 165 | TAGS="$(git ls-remote origin 2>/dev/null | grep -v "\^{}" | grep "refs/tags/" | cut -f3 -d/)" 166 | 167 | if [ -n "$VERBOSE" ]; 168 | then 169 | echo "${DEBUG} TAGS=\"${TAGS}\"" 170 | fi 171 | fi 172 | 173 | for SPLIT in $SPLITS 174 | do 175 | SUBPATH=$(echo "$SPLIT" | cut -f1 -d:) 176 | REMOTE_URL=$(echo "$SPLIT" | cut -f2- -d:) 177 | REMOTE_NAME=$(echo "$SPLIT" | git hash-object --stdin) 178 | 179 | if [ -n "$VERBOSE" ]; 180 | then 181 | echo "${DEBUG} SUBPATH=${SUBPATH}" 182 | echo "${DEBUG} REMOTE_URL=${REMOTE_URL}" 183 | echo "${DEBUG} REMOTE_NAME=${REMOTE_NAME}" 184 | fi 185 | 186 | if ! git remote | grep "^${REMOTE_NAME}$" >/dev/null 187 | then 188 | git remote add "$REMOTE_NAME" "$REMOTE_URL" 189 | 190 | if [ -n "$VERBOSE" ]; 191 | then 192 | echo "${DEBUG} git remote add \"${REMOTE_NAME}\" \"${REMOTE_URL}\"" 193 | fi 194 | fi 195 | 196 | 197 | say "Syncing ${SUBPATH} -> ${REMOTE_URL}" 198 | 199 | for HEAD in $HEADS 200 | do 201 | if [ -n "$VERBOSE" ]; 202 | then 203 | echo "${DEBUG} git show-ref --quiet --verify -- \"refs/remotes/origin/${HEAD}\"" 204 | fi 205 | 206 | if ! git show-ref --quiet --verify -- "refs/remotes/origin/${HEAD}" 207 | then 208 | say " - skipping head '${HEAD}' (does not exist)" 209 | continue 210 | fi 211 | LOCAL_BRANCH="${REMOTE_NAME}-branch-${HEAD}" 212 | 213 | if [ -n "$VERBOSE" ]; 214 | then 215 | echo "${DEBUG} LOCAL_BRANCH=\"${LOCAL_BRANCH}\"" 216 | fi 217 | 218 | say " - syncing branch '${HEAD}'" 219 | 220 | git checkout master >/dev/null 2>&1 221 | git branch -D "$LOCAL_BRANCH" >/dev/null 2>&1 222 | git branch -D "${LOCAL_BRANCH}-checkout" >/dev/null 2>&1 223 | git checkout -b "${LOCAL_BRANCH}-checkout" "origin/${HEAD}" >/dev/null 2>&1 224 | git subtree split -q --prefix="$SUBPATH" --branch="$LOCAL_BRANCH" "origin/${HEAD}" >/dev/null 225 | RETURNCODE=$? 226 | 227 | if [ -n "$VERBOSE" ]; 228 | then 229 | echo "${DEBUG} git checkout master >/dev/null 2>&1" 230 | echo "${DEBUG} git branch -D \"$LOCAL_BRANCH\" >/dev/null 2>&1" 231 | echo "${DEBUG} git branch -D \"${LOCAL_BRANCH}-checkout\" >/dev/null 2>&1" 232 | echo "${DEBUG} git checkout -b \"${LOCAL_BRANCH}-checkout\" \"origin/${HEAD}\" >/dev/null 2>&1" 233 | echo "${DEBUG} git subtree split -q --prefix=\"$SUBPATH\" --branch=\"$LOCAL_BRANCH\" \"origin/${HEAD}\" >/dev/null" 234 | fi 235 | 236 | if [ $RETURNCODE -eq 0 ] 237 | then 238 | PUSH_CMD="git push -q ${DRY_RUN} --force $REMOTE_NAME ${LOCAL_BRANCH}:${HEAD}" 239 | 240 | if [ -n "$VERBOSE" ]; 241 | then 242 | echo "${DEBUG} $PUSH_CMD" 243 | fi 244 | 245 | if [ -n "$DRY_RUN" ] 246 | then 247 | echo \# $PUSH_CMD 248 | $PUSH_CMD 249 | else 250 | $PUSH_CMD 251 | fi 252 | fi 253 | done 254 | 255 | for TAG in $TAGS 256 | do 257 | if [ -n "$VERBOSE" ]; 258 | then 259 | echo "${DEBUG} git show-ref --quiet --verify -- \"refs/tags/${TAG}\"" 260 | fi 261 | 262 | if ! git show-ref --quiet --verify -- "refs/tags/${TAG}" 263 | then 264 | say " - skipping tag '${TAG}' (does not exist)" 265 | continue 266 | fi 267 | LOCAL_TAG="${REMOTE_NAME}-tag-${TAG}" 268 | 269 | if [ -n "$VERBOSE" ]; 270 | then 271 | echo "${DEBUG} LOCAL_TAG="${LOCAL_TAG}"" 272 | fi 273 | 274 | if git branch | grep "${LOCAL_TAG}$" >/dev/null && [ -z "$REBUILD_TAGS" ] 275 | then 276 | say " - skipping tag '${TAG}' (already synced)" 277 | continue 278 | fi 279 | 280 | if [ -n "$VERBOSE" ]; 281 | then 282 | echo "${DEBUG} git branch | grep \"${LOCAL_TAG}$\" >/dev/null && [ -z \"${REBUILD_TAGS}\" ]" 283 | fi 284 | 285 | say " - syncing tag '${TAG}'" 286 | say " - deleting '${LOCAL_TAG}'" 287 | git branch -D "$LOCAL_TAG" >/dev/null 2>&1 288 | 289 | if [ -n "$VERBOSE" ]; 290 | then 291 | echo "${DEBUG} git branch -D \"${LOCAL_TAG}\" >/dev/null 2>&1" 292 | fi 293 | 294 | say " - subtree split for '${TAG}'" 295 | git subtree split -q --annotate="${ANNOTATE}" --prefix="$SUBPATH" --branch="$LOCAL_TAG" "$TAG" >/dev/null 296 | RETURNCODE=$? 297 | 298 | if [ -n "$VERBOSE" ]; 299 | then 300 | echo "${DEBUG} git subtree split -q --annotate=\"${ANNOTATE}\" --prefix=\"$SUBPATH\" --branch=\"$LOCAL_TAG\" \"$TAG\" >/dev/null" 301 | fi 302 | 303 | say " - subtree split for '${TAG}' [DONE]" 304 | if [ $RETURNCODE -eq 0 ] 305 | then 306 | PUSH_CMD="git push -q ${DRY_RUN} --force ${REMOTE_NAME} ${LOCAL_TAG}:refs/tags/${TAG}" 307 | 308 | if [ -n "$VERBOSE" ]; 309 | then 310 | echo "${DEBUG} PUSH_CMD=\"${PUSH_CMD}\"" 311 | fi 312 | 313 | if [ -n "$DRY_RUN" ] 314 | then 315 | echo \# $PUSH_CMD 316 | $PUSH_CMD 317 | else 318 | $PUSH_CMD 319 | fi 320 | fi 321 | done 322 | done 323 | 324 | popd >/dev/null 325 | } 326 | 327 | subsplit_update() 328 | { 329 | subsplit_require_work_dir 330 | 331 | say "Updating subsplit from origin" 332 | 333 | git fetch -q -t origin 334 | git checkout master 335 | git reset --hard origin/master 336 | 337 | if [ -n "$VERBOSE" ]; 338 | then 339 | echo "${DEBUG} git fetch -q -t origin" 340 | echo "${DEBUG} git checkout master" 341 | echo "${DEBUG} git reset --hard origin/master" 342 | fi 343 | 344 | popd >/dev/null 345 | } 346 | 347 | subsplit_main "$@" 348 | --------------------------------------------------------------------------------