| 1 | #!/usr/bin/env bash
|
| 2 | set -eu -o pipefail
|
| 3 |
|
| 4 | # This is a complete Cartesian product within a deliberately finite model:
|
| 5 | #
|
| 6 | # * one merge base;
|
| 7 | # * one regular file whose identity can be modified, deleted, or renamed;
|
| 8 | # * one occupied rename destination;
|
| 9 | # * two initially free destinations;
|
| 10 | # * at most one atomic operation per side;
|
| 11 | # * exact rename detection and the built-in text merge driver.
|
| 12 | #
|
| 13 | # Modes, symlinks, submodules, copies, attributes, recursive merge bases, and
|
| 14 | # multiple simultaneous operations are separate dimensions. Including them here
|
| 15 | # would obscure the state-machine coverage with a mostly redundant cross product.
|
| 16 | #
|
| 17 | # The atomic states are:
|
| 18 | #
|
| 19 | # * keep, modify, or delete the source;
|
| 20 | # * rename it unchanged to either free destination;
|
| 21 | # * rename it with a content change to `free-a`;
|
| 22 | # * add an unrelated file at either free destination;
|
| 23 | # * modify or delete the occupied destination;
|
| 24 | # * replace the occupied destination with the source, unchanged or modified;
|
| 25 | # * replace the source file with a directory and child;
|
| 26 | # * rename the source away and then create a directory and child at its old path.
|
| 27 | #
|
| 28 | # `free-b` exists to distinguish same-destination from different-destination
|
| 29 | # interactions. Repeating the modified-rename state for `free-b` would only
|
| 30 | # duplicate `free-a` cases under a path-name substitution.
|
| 31 | operations=(
|
| 32 | keep
|
| 33 | modify-source
|
| 34 | delete-source
|
| 35 | rename-free-a
|
| 36 | rename-free-b
|
| 37 | rename-modify-free-a
|
| 38 | add-free-a
|
| 39 | add-free-b
|
| 40 | modify-occupied
|
| 41 | delete-occupied
|
| 42 | replace-occupied
|
| 43 | replace-modify-occupied
|
| 44 | file-to-dir
|
| 45 | rename-and-file-to-dir
|
| 46 | )
|
| 47 |
|
| 48 | function tick () {
|
| 49 | if test -z "${tick+set}"
|
| 50 | then
|
| 51 | tick=1112911993
|
| 52 | else
|
| 53 | tick=$((tick + 60))
|
| 54 | fi
|
| 55 | GIT_COMMITTER_DATE="$tick -0700"
|
| 56 | GIT_AUTHOR_DATE="$tick -0700"
|
| 57 | export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
|
| 58 | }
|
| 59 |
|
| 60 | function apply_operation () {
|
| 61 | local side=${1:?side}
|
| 62 | local operation=${2:?operation}
|
| 63 | local payload="payload-${side}-${operation}"
|
| 64 |
|
| 65 | case "$operation" in
|
| 66 | keep)
|
| 67 | ;;
|
| 68 | modify-source)
|
| 69 | echo "$payload" >>source
|
| 70 | ;;
|
| 71 | delete-source)
|
| 72 | git rm source
|
| 73 | ;;
|
| 74 | rename-free-a)
|
| 75 | git mv source free-a
|
| 76 | ;;
|
| 77 | rename-free-b)
|
| 78 | git mv source free-b
|
| 79 | ;;
|
| 80 | rename-modify-free-a)
|
| 81 | echo "$payload" >>source
|
| 82 | git mv source free-a
|
| 83 | ;;
|
| 84 | add-free-a)
|
| 85 | echo "$payload" >free-a
|
| 86 | ;;
|
| 87 | add-free-b)
|
| 88 | echo "$payload" >free-b
|
| 89 | ;;
|
| 90 | modify-occupied)
|
| 91 | echo "$payload" >>occupied
|
| 92 | ;;
|
| 93 | delete-occupied)
|
| 94 | git rm occupied
|
| 95 | ;;
|
| 96 | replace-occupied)
|
| 97 | git rm occupied
|
| 98 | git mv source occupied
|
| 99 | ;;
|
| 100 | replace-modify-occupied)
|
| 101 | git rm occupied
|
| 102 | echo "$payload" >>source
|
| 103 | git mv source occupied
|
| 104 | ;;
|
| 105 | file-to-dir)
|
| 106 | git rm source
|
| 107 | mkdir source
|
| 108 | echo "$payload" >source/child
|
| 109 | ;;
|
| 110 | rename-and-file-to-dir)
|
| 111 | git mv source moved
|
| 112 | mkdir source
|
| 113 | echo "$payload" >source/child
|
| 114 | ;;
|
| 115 | *)
|
| 116 | echo "unknown operation: $operation" >&2
|
| 117 | return 1
|
| 118 | ;;
|
| 119 | esac
|
| 120 | }
|
| 121 |
|
| 122 | git init --object-format=sha1 matrix
|
| 123 | (cd matrix
|
| 124 | git config user.name "Cartesian Merge Baseline"
|
| 125 | git config user.email "[email protected]"
|
| 126 |
|
| 127 | printf '%s\n' source source source source source >source
|
| 128 | printf '%s\n' occupied occupied occupied occupied occupied >occupied
|
| 129 | git add .
|
| 130 | tick
|
| 131 | git commit -m "base"
|
| 132 | git branch base
|
| 133 | git rev-parse HEAD^{tree} >.git/base.tree
|
| 134 |
|
| 135 | # Make a side-specific commit for every state. Content-changing states include
|
| 136 | # a unique payload so the Rust test can detect silently discarded changes.
|
| 137 | for side in A B
|
| 138 | do
|
| 139 | for operation in "${operations[@]}"
|
| 140 | do
|
| 141 | git checkout --detach --force base
|
| 142 | apply_operation "$side" "$operation"
|
| 143 | git add -A
|
| 144 | tick
|
| 145 | git commit --allow-empty -m "$side: $operation"
|
| 146 | git branch "$side-$operation"
|
| 147 | done
|
| 148 | done
|
| 149 |
|
| 150 | : >cases.tsv
|
| 151 | for ((left = 0; left < ${#operations[@]}; left++))
|
| 152 | do
|
| 153 | for ((right = left; right < ${#operations[@]}; right++))
|
| 154 | do
|
| 155 | left_operation=${operations[$left]}
|
| 156 | right_operation=${operations[$right]}
|
| 157 | case_name="${left_operation}--${right_operation}"
|
| 158 | forward_file="${case_name}-A-B.merge-info"
|
| 159 | reverse_file="${case_name}-B-A.merge-info"
|
| 160 |
|
| 161 | if git merge-tree -z --write-tree \
|
| 162 | "A-$left_operation" "B-$right_operation" >".git/$forward_file"
|
| 163 | then
|
| 164 | forward_status=clean
|
| 165 | else
|
| 166 | forward_status=conflicted
|
| 167 | fi
|
| 168 | if git merge-tree -z --write-tree \
|
| 169 | "B-$right_operation" "A-$left_operation" >".git/$reverse_file"
|
| 170 | then
|
| 171 | reverse_status=clean
|
| 172 | else
|
| 173 | reverse_status=conflicted
|
| 174 | fi
|
| 175 |
|
| 176 | printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
| 177 | "$left_operation" \
|
| 178 | "$right_operation" \
|
| 179 | "$(git rev-parse "A-$left_operation")" \
|
| 180 | "$(git rev-parse "B-$right_operation")" \
|
| 181 | "$forward_file" \
|
| 182 | "$forward_status" \
|
| 183 | "$reverse_file" \
|
| 184 | "$reverse_status" >>cases.tsv
|
| 185 | done
|
| 186 | done
|
| 187 | )
|
| 188 |
|
| 189 | function apply_submodule_operation () {
|
| 190 | local side=${1:?side}
|
| 191 | local operation=${2:?operation}
|
| 192 | local payload="payload-${side}-${operation}"
|
| 193 | local changed_source changed_occupied added
|
| 194 | if test "$side" = A
|
| 195 | then
|
| 196 | changed_source=$source_a
|
| 197 | changed_occupied=$occupied_a
|
| 198 | added=$added_a
|
| 199 | else
|
| 200 | changed_source=$source_b
|
| 201 | changed_occupied=$occupied_b
|
| 202 | added=$added_b
|
| 203 | fi
|
| 204 |
|
| 205 | case "$operation" in
|
| 206 | keep)
|
| 207 | ;;
|
| 208 | modify-source)
|
| 209 | git update-index --add --cacheinfo "160000,$changed_source,source"
|
| 210 | ;;
|
| 211 | delete-source)
|
| 212 | git update-index --force-remove source
|
| 213 | ;;
|
| 214 | rename-free-a)
|
| 215 | git update-index --force-remove source
|
| 216 | git update-index --add --cacheinfo "160000,$source_base,free-a"
|
| 217 | ;;
|
| 218 | rename-free-b)
|
| 219 | git update-index --force-remove source
|
| 220 | git update-index --add --cacheinfo "160000,$source_base,free-b"
|
| 221 | ;;
|
| 222 | rename-modify-free-a)
|
| 223 | git update-index --force-remove source
|
| 224 | git update-index --add --cacheinfo "160000,$changed_source,free-a"
|
| 225 | ;;
|
| 226 | add-free-a)
|
| 227 | git update-index --add --cacheinfo "160000,$added,free-a"
|
| 228 | ;;
|
| 229 | add-free-b)
|
| 230 | git update-index --add --cacheinfo "160000,$added,free-b"
|
| 231 | ;;
|
| 232 | modify-occupied)
|
| 233 | git update-index --add --cacheinfo "160000,$changed_occupied,occupied"
|
| 234 | ;;
|
| 235 | delete-occupied)
|
| 236 | git update-index --force-remove occupied
|
| 237 | ;;
|
| 238 | replace-occupied)
|
| 239 | git update-index --force-remove source
|
| 240 | git update-index --add --cacheinfo "160000,$source_base,occupied"
|
| 241 | ;;
|
| 242 | replace-modify-occupied)
|
| 243 | git update-index --force-remove source
|
| 244 | git update-index --add --cacheinfo "160000,$changed_source,occupied"
|
| 245 | ;;
|
| 246 | file-to-dir)
|
| 247 | git update-index --force-remove source
|
| 248 | mkdir -p source
|
| 249 | echo "$payload" >source/child
|
| 250 | git add source/child
|
| 251 | ;;
|
| 252 | rename-and-file-to-dir)
|
| 253 | git update-index --force-remove source
|
| 254 | git update-index --add --cacheinfo "160000,$source_base,moved"
|
| 255 | mkdir -p source
|
| 256 | echo "$payload" >source/child
|
| 257 | git add source/child
|
| 258 | ;;
|
| 259 | *)
|
| 260 | echo "unknown submodule operation: $operation" >&2
|
| 261 | return 1
|
| 262 | ;;
|
| 263 | esac
|
| 264 | }
|
| 265 |
|
| 266 | # Repeat the same state machine with gitlinks. Commit objects live in this repository merely to provide deterministic,
|
| 267 | # valid gitlink IDs; the tree merger still treats their ancestry as a caller-side submodule concern.
|
| 268 | git init --object-format=sha1 matrix-submodule
|
| 269 | (cd matrix-submodule
|
| 270 | git config user.name "Cartesian Submodule Baseline"
|
| 271 | git config user.email "[email protected]"
|
| 272 |
|
| 273 | empty_tree=$(git mktree </dev/null)
|
| 274 | tick
|
| 275 | source_base=$(printf 'submodule source base\n' | git commit-tree "$empty_tree")
|
| 276 | tick
|
| 277 | occupied_base=$(printf 'submodule occupied base\n' | git commit-tree "$empty_tree")
|
| 278 | tick
|
| 279 | source_a=$(printf 'submodule source A\n' | git commit-tree "$empty_tree" -p "$source_base")
|
| 280 | tick
|
| 281 | source_b=$(printf 'submodule source B\n' | git commit-tree "$empty_tree" -p "$source_base")
|
| 282 | tick
|
| 283 | occupied_a=$(printf 'submodule occupied A\n' | git commit-tree "$empty_tree" -p "$occupied_base")
|
| 284 | tick
|
| 285 | occupied_b=$(printf 'submodule occupied B\n' | git commit-tree "$empty_tree" -p "$occupied_base")
|
| 286 | tick
|
| 287 | added_a=$(printf 'submodule addition A\n' | git commit-tree "$empty_tree")
|
| 288 | tick
|
| 289 | added_b=$(printf 'submodule addition B\n' | git commit-tree "$empty_tree")
|
| 290 |
|
| 291 | git update-index --add --cacheinfo "160000,$source_base,source"
|
| 292 | git update-index --add --cacheinfo "160000,$occupied_base,occupied"
|
| 293 | tick
|
| 294 | git commit -m "gitlink base"
|
| 295 | git branch base
|
| 296 |
|
| 297 | for side in A B
|
| 298 | do
|
| 299 | for operation in "${operations[@]}"
|
| 300 | do
|
| 301 | git checkout --detach --force base
|
| 302 | apply_submodule_operation "$side" "$operation"
|
| 303 | tick
|
| 304 | git commit --allow-empty -m "$side: $operation"
|
| 305 | git branch "$side-$operation"
|
| 306 | done
|
| 307 | done
|
| 308 |
|
| 309 | : >cases.tsv
|
| 310 | for ((left = 0; left < ${#operations[@]}; left++))
|
| 311 | do
|
| 312 | for ((right = left; right < ${#operations[@]}; right++))
|
| 313 | do
|
| 314 | left_operation=${operations[$left]}
|
| 315 | right_operation=${operations[$right]}
|
| 316 | case_name="${left_operation}--${right_operation}"
|
| 317 | forward_file="${case_name}-A-B.merge-info"
|
| 318 | reverse_file="${case_name}-B-A.merge-info"
|
| 319 |
|
| 320 | if git merge-tree -z --write-tree \
|
| 321 | "A-$left_operation" "B-$right_operation" >".git/$forward_file"
|
| 322 | then
|
| 323 | forward_status=clean
|
| 324 | else
|
| 325 | forward_status=conflicted
|
| 326 | fi
|
| 327 | if git merge-tree -z --write-tree \
|
| 328 | "B-$right_operation" "A-$left_operation" >".git/$reverse_file"
|
| 329 | then
|
| 330 | reverse_status=clean
|
| 331 | else
|
| 332 | reverse_status=conflicted
|
| 333 | fi
|
| 334 |
|
| 335 | printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
| 336 | "$left_operation" \
|
| 337 | "$right_operation" \
|
| 338 | "$(git rev-parse "A-$left_operation")" \
|
| 339 | "$(git rev-parse "B-$right_operation")" \
|
| 340 | "$forward_file" \
|
| 341 | "$forward_status" \
|
| 342 | "$reverse_file" \
|
| 343 | "$reverse_status" >>cases.tsv
|
| 344 | done
|
| 345 | done
|
| 346 | )
|