Imported Upstream version 2.5.1
[scm/test.git] / t / testhelpers.sh
1 #!/usr/bin/env bash
2
3 # assert_pointer confirms that the pointer in the repository for $path in the
4 # given $ref matches the given $oid and $size.
5 #
6 #   $ assert_pointer "master" "path/to/file" "some-oid" 123
7 assert_pointer() {
8   local ref="$1"
9   local path="$2"
10   local oid="$3"
11   local size="$4"
12
13   gitblob=$(git ls-tree -lrz "$ref" |
14     while read -r -d $'\0' x; do
15       echo $x
16     done |
17     grep "$path" | cut -f 3 -d " ")
18
19   actual=$(git cat-file -p $gitblob)
20   expected=$(pointer $oid $size)
21
22   if [ "$expected" != "$actual" ]; then
23     exit 1
24   fi
25 }
26
27 # refute_pointer confirms that the file in the repository for $path in the
28 # given $ref is _not_ a pointer.
29 #
30 #   $ refute_pointer "master" "path/to/file"
31 refute_pointer() {
32   local ref="$1"
33   local path="$2"
34
35   gitblob=$(git ls-tree -lrz "$ref" |
36     while read -r -d $'\0' x; do
37       echo $x
38     done |
39     grep "$path" | cut -f 3 -d " ")
40
41   file=$(git cat-file -p $gitblob)
42   version="version https://git-lfs.github.com/spec/v[0-9]"
43   oid="oid sha256:[0-9a-f]\{64\}"
44   size="size [0-9]*"
45   regex="$version.*$oid.*$size"
46
47   if echo $file | grep -q "$regex"; then
48     exit 1
49   fi
50 }
51
52 # assert_local_object confirms that an object file is stored for the given oid &
53 # has the correct size
54 # $ assert_local_object "some-oid" size
55 assert_local_object() {
56   local oid="$1"
57   local size="$2"
58   local cfg=`git lfs env | grep LocalMediaDir`
59   local f="${cfg:14}/${oid:0:2}/${oid:2:2}/$oid"
60   actualsize=$(wc -c <"$f" | tr -d '[[:space:]]')
61   if [ "$size" != "$actualsize" ]; then
62     exit 1
63   fi
64 }
65
66 # refute_local_object confirms that an object file is NOT stored for an oid.
67 # If "$size" is given as the second argument, assert that the file exists _and_
68 # that it does _not_ the expected size
69 #
70 # $ refute_local_object "some-oid"
71 # $ refute_local_object "some-oid" "123"
72 refute_local_object() {
73   local oid="$1"
74   local size="$2"
75   local cfg=`git lfs env | grep LocalMediaDir`
76   local regex="LocalMediaDir=(\S+)"
77   local f="${cfg:14}/${oid:0:2}/${oid:2:2}/$oid"
78   if [ -e $f ]; then
79     if [ -z "$size" ]; then
80       exit 1
81     fi
82
83     actual_size="$(wc -c < "$f" | awk '{ print $1 }')"
84     if [ "$size" -eq "$actual_size" ]; then
85       echo >&2 "fatal: expected object $oid not to have size: $size"
86       exit 1
87     fi
88   fi
89 }
90
91 # delete_local_object deletes the local storage for an oid
92 # $ delete_local_object "some-oid"
93 delete_local_object() {
94   local oid="$1"
95   local cfg=`git lfs env | grep LocalMediaDir`
96   local f="${cfg:14}/${oid:0:2}/${oid:2:2}/$oid"
97   rm "$f"
98 }
99
100 # corrupt_local_object corrupts the local storage for an oid
101 # $ corrupt_local_object "some-oid"
102 corrupt_local_object() {
103   local oid="$1"
104   local cfg=`git lfs env | grep LocalMediaDir`
105   local f="${cfg:14}/${oid:0:2}/${oid:2:2}/$oid"
106   cp /dev/null "$f"
107 }
108
109
110 # check that the object does not exist in the git lfs server. HTTP log is
111 # written to http.log. JSON output is written to http.json.
112 #
113 #   $ refute_server_object "reponame" "oid"
114 refute_server_object() {
115   local reponame="$1"
116   local oid="$2"
117   curl -v "$GITSERVER/$reponame.git/info/lfs/objects/batch" \
118     -u "user:pass" \
119     -o http.json \
120     -d "{\"operation\":\"download\",\"objects\":[{\"oid\":\"$oid\"}]}" \
121     -H "Accept: application/vnd.git-lfs+json" \
122     -H "X-Check-Object: 1" \
123     -H "X-Ignore-Retries: true" 2>&1 |
124     tee http.log
125
126   [ "0" = "$(grep -c "download" http.json)" ] || {
127     cat http.json
128     exit 1
129   }
130 }
131
132 # Delete an object on the lfs server. HTTP log is
133 # written to http.log. JSON output is written to http.json.
134 #
135 #   $ delete_server_object "reponame" "oid"
136 delete_server_object() {
137   local reponame="$1"
138   local oid="$2"
139   curl -v "$GITSERVER/$reponame.git/info/lfs/objects/$oid" \
140     -X DELETE \
141     -u "user:pass" \
142     -o http.json \
143     -H "Accept: application/vnd.git-lfs+json" 2>&1 |
144     tee http.log
145
146   grep "200 OK" http.log
147 }
148
149 # check that the object does exist in the git lfs server. HTTP log is written
150 # to http.log. JSON output is written to http.json.
151 assert_server_object() {
152   local reponame="$1"
153   local oid="$2"
154   local refspec="$3"
155   curl -v "$GITSERVER/$reponame.git/info/lfs/objects/batch" \
156     -u "user:pass" \
157     -o http.json \
158     -d "{\"operation\":\"download\",\"objects\":[{\"oid\":\"$oid\"}],\"ref\":{\"name\":\"$refspec\"}}" \
159     -H "Accept: application/vnd.git-lfs+json" \
160     -H "X-Check-Object: 1" \
161     -H "X-Ignore-Retries: true" 2>&1 |
162     tee http.log
163   grep "200 OK" http.log
164
165   grep "download" http.json || {
166     cat http.json
167     exit 1
168   }
169 }
170
171 # This asserts the lock path and returns the lock ID by parsing the response of
172 #
173 #   git lfs lock --json <path>
174 assert_lock() {
175   local log="$1"
176   local path="$2"
177
178   if [ $(grep -c "\"path\":\"$path\"" "$log") -eq 0 ]; then
179     echo "path '$path' not found in:"
180     cat "$log"
181     exit 1
182   fi
183
184   local jsonid=$(grep -oh "\"id\":\"\w\+\"" "$log")
185   echo "${jsonid:3}" | tr -d \"\:
186 }
187
188 # assert that a lock with the given ID exists on the test server
189 assert_server_lock() {
190   local reponame="$1"
191   local id="$2"
192   local refspec="$3"
193
194   curl -v "$GITSERVER/$reponame.git/info/lfs/locks?refspec=$refspec" \
195     -u "user:pass" \
196     -o http.json \
197     -H "Accept:application/vnd.git-lfs+json" 2>&1 |
198     tee http.log
199
200   grep "200 OK" http.log
201   grep "$id" http.json || {
202     cat http.json
203     exit 1
204   }
205 }
206
207 # refute that a lock with the given ID exists on the test server
208 refute_server_lock() {
209   local reponame="$1"
210   local id="$2"
211   local refspec="$3"
212
213   curl -v "$GITSERVER/$reponame.git/info/lfs/locks?refspec=$refspec" \
214     -u "user:pass" \
215     -o http.json \
216     -H "Accept:application/vnd.git-lfs+json" 2>&1 | tee http.log
217
218   grep "200 OK" http.log
219
220   [ $(grep -c "$id" http.json) -eq 0 ]
221 }
222
223 # Assert that .gitattributes contains a given attribute N times
224 assert_attributes_count() {
225   local fileext="$1"
226   local attrib="$2"
227   local count="$3"
228
229   pattern="\(*.\)\?$fileext\(.*\)$attrib"
230   actual=$(grep -e "$pattern" .gitattributes | wc -l)
231   if [ "$(printf "%d" "$actual")" != "$count" ]; then
232     echo "wrong number of $attrib entries for $fileext"
233     echo "expected: $count actual: $actual"
234     cat .gitattributes
235     exit 1
236   fi
237 }
238
239 assert_file_writeable() {
240   ls -l "$1" | grep -e "^-rw"
241 }
242
243 refute_file_writeable() {
244   ls -l "$1" | grep -e "^-r-"
245 }
246
247 git_root() {
248   git rev-parse --show-toplevel 2>/dev/null
249 }
250
251 dot_git_dir() {
252   echo "$(git_root)/.git"
253 }
254
255 assert_hooks() {
256   local git_root="$1"
257
258   if [ -z "$git_root" ]; then
259     echo >&2 "fatal: (assert_hooks) not in git repository"
260     exit 1
261   fi
262
263   [ -x "$git_root/hooks/post-checkout" ]
264   [ -x "$git_root/hooks/post-commit" ]
265   [ -x "$git_root/hooks/post-merge" ]
266   [ -x "$git_root/hooks/pre-push" ]
267 }
268
269 assert_clean_status() {
270   status="$(git status)"
271   echo "$status" | grep "working tree clean" || {
272     echo $status
273     git lfs status
274   }
275 }
276
277 # pointer returns a string Git LFS pointer file.
278 #
279 #   $ pointer abc-some-oid 123 <version>
280 #   > version ...
281 pointer() {
282   local oid=$1
283   local size=$2
284   local version=${3:-https://git-lfs.github.com/spec/v1}
285   printf "version %s
286 oid sha256:%s
287 size %s
288 " "$version" "$oid" "$size"
289 }
290
291 # wait_for_file simply sleeps until a file exists.
292 #
293 #   $ wait_for_file "path/to/upcoming/file"
294 wait_for_file() {
295   local filename="$1"
296   n=0
297   wait_time=1
298   while [ $n -lt 17 ]; do
299     if [ -s $filename ]; then
300       return 0
301     fi
302
303     sleep $wait_time
304     n=`expr $n + 1`
305     if [ $wait_time -lt 4 ]; then
306       wait_time=`expr $wait_time \* 2`
307     fi
308   done
309
310   echo "$filename did not appear after 60 seconds."
311   return 1
312 }
313
314 # setup_remote_repo initializes a bare Git repository that is accessible through
315 # the test Git server. The `pwd` is set to the repository's directory, in case
316 # further commands need to be run. This server is running for every test in an
317 # integration run, so every test file should setup its own remote repository to
318 # avoid conflicts.
319 #
320 #   $ setup_remote_repo "some-name"
321 #
322 setup_remote_repo() {
323   local reponame="$1"
324   echo "set up remote git repository: $reponame"
325   repodir="$REMOTEDIR/$reponame.git"
326   mkdir -p "$repodir"
327   cd "$repodir"
328   git init --bare
329   git config http.receivepack true
330   git config receive.denyCurrentBranch ignore
331 }
332
333 # creates a bare remote repository for a local clone. Useful to test pushing to
334 # a fresh remote server.
335 #
336 #   $ setup_alternate_remote "$reponame-whatever"
337 #   $ setup_alternate_remote "$reponame-whatever" "other-remote-name"
338 #
339 setup_alternate_remote() {
340   local newRemoteName=$1
341   local remote=${2:-origin}
342
343   wd=`pwd`
344
345   setup_remote_repo "$newRemoteName"
346   cd $wd
347   git remote rm "$remote"
348   git remote add "$remote" "$GITSERVER/$newRemoteName"
349 }
350
351 # clone_repo clones a repository from the test Git server to the subdirectory
352 # $dir under $TRASHDIR. setup_remote_repo() needs to be run first. Output is
353 # written to clone.log.
354 clone_repo() {
355   cd "$TRASHDIR"
356
357   local reponame="$1"
358   local dir="$2"
359   echo "clone local git repository $reponame to $dir"
360   out=$(git clone "$GITSERVER/$reponame" "$dir" 2>&1)
361   cd "$dir"
362
363   git config credential.helper lfstest
364   echo "$out" > clone.log
365   echo "$out"
366 }
367
368 # clone_repo_url clones a Git repository to the subdirectory $dir under $TRASHDIR.
369 # setup_remote_repo() needs to be run first. Output is written to clone.log.
370 clone_repo_url() {
371   cd "$TRASHDIR"
372
373   local repo="$1"
374   local dir="$2"
375   echo "clone git repository $repo to $dir"
376   out=$(git clone "$repo" "$dir" 2>&1)
377   cd "$dir"
378
379   git config credential.helper lfstest
380   echo "$out" > clone.log
381   echo "$out"
382 }
383
384 # clone_repo_ssl clones a repository from the test Git server to the subdirectory
385 # $dir under $TRASHDIR, using the SSL endpoint.
386 # setup_remote_repo() needs to be run first. Output is written to clone_ssl.log.
387 clone_repo_ssl() {
388   cd "$TRASHDIR"
389
390   local reponame="$1"
391   local dir="$2"
392   echo "clone local git repository $reponame to $dir"
393   out=$(git clone "$SSLGITSERVER/$reponame" "$dir" 2>&1)
394   cd "$dir"
395
396   git config credential.helper lfstest
397
398   echo "$out" > clone_ssl.log
399   echo "$out"
400 }
401
402 # clone_repo_clientcert clones a repository from the test Git server to the subdirectory
403 # $dir under $TRASHDIR, using the client cert endpoint.
404 # setup_remote_repo() needs to be run first. Output is written to clone_client_cert.log.
405 clone_repo_clientcert() {
406   cd "$TRASHDIR"
407
408   local reponame="$1"
409   local dir="$2"
410   echo "clone $CLIENTCERTGITSERVER/$reponame to $dir"
411   set +e
412   out=$(git clone "$CLIENTCERTGITSERVER/$reponame" "$dir" 2>&1)
413   res="${PIPESTATUS[0]}"
414   set -e
415
416   if [ "0" -eq "$res" ]; then
417     cd "$dir"
418     echo "$out" > clone_client_cert.log
419
420     git config credential.helper lfstest
421     exit 0
422   fi
423
424   echo "$out" > clone_client_cert.log
425   if [ $(grep -c "NSInvalidArgumentException" clone_client_cert.log) -gt 0 ]; then
426     echo "client-cert-mac-openssl" > clone_client_cert.log
427     exit 0
428   fi
429
430   exit 1
431 }
432
433 # setup_remote_repo_with_file creates a remote repo, clones it locally, commits
434 # a file tracked by LFS, and pushes it to the remote:
435 #
436 #     setup_remote_repo_with_file "reponame" "filename"
437 setup_remote_repo_with_file() {
438   local reponame="$1"
439   local filename="$2"
440   local dirname="$(dirname "$filename")"
441
442   setup_remote_repo "$reponame"
443   clone_repo "$reponame" "clone_$reponame"
444
445   mkdir -p "$dirname"
446
447   git lfs track "$filename"
448   echo "$filename" > "$filename"
449   git add .gitattributes $filename
450   git commit -m "add $filename" | tee commit.log
451
452   grep "master (root-commit)" commit.log
453   grep "2 files changed" commit.log
454   grep "create mode 100644 $filename" commit.log
455   grep "create mode 100644 .gitattributes" commit.log
456
457   git push origin master 2>&1 | tee push.log
458   grep "master -> master" push.log
459 }
460
461 # substring_position returns the position of a substring in a 1-indexed search
462 # space.
463 #
464 #     [ "$(substring_position "foo bar baz" "baz")" -eq "9" ]
465 substring_position() {
466   local str="$1"
467   local substr="$2"
468
469   # 1) Print the string...
470   # 2) Remove the substring and everything after it
471   # 3) Count the number of characters (bytes) left, i.e., the offset of the
472   #    string we were looking for.
473
474   echo "$str" \
475     | sed "s/$substr.*$//" \
476     | wc -c
477 }
478
479 # repo_endpoint returns the LFS endpoint for a given server and repository.
480 #
481 #     [ "$GITSERVER/example/repo.git/info/lfs" = "$(repo_endpoint $GITSERVER example-repo)" ]
482 repo_endpoint() {
483   local server="$1"
484   local repo="$2"
485
486   echo "$server/$repo.git/info/lfs"
487 }
488
489 # setup initializes the clean, isolated environment for integration tests.
490 setup() {
491   cd "$ROOTDIR"
492
493   if [ ! -d "$REMOTEDIR" ]; then
494     mkdir "$REMOTEDIR"
495   fi
496
497   echo "# Git LFS: ${LFS_BIN:-$(which git-lfs)}"
498   git lfs version | sed -e 's/^/# /g'
499   git version | sed -e 's/^/# /g'
500
501   if [ -z "$GIT_LFS_NO_TEST_COUNT" ]; then
502     LFSTEST_URL="$LFS_URL_FILE" \
503     LFSTEST_SSL_URL="$LFS_SSL_URL_FILE" \
504     LFSTEST_CLIENT_CERT_URL="$LFS_CLIENT_CERT_URL_FILE" \
505     LFSTEST_DIR="$REMOTEDIR" \
506     LFSTEST_CERT="$LFS_CERT_FILE" \
507     LFSTEST_CLIENT_CERT="$LFS_CLIENT_CERT_FILE" \
508     LFSTEST_CLIENT_KEY="$LFS_CLIENT_KEY_FILE" \
509       lfstest-count-tests increment
510   fi
511
512   wait_for_file "$LFS_URL_FILE"
513   wait_for_file "$LFS_SSL_URL_FILE"
514   wait_for_file "$LFS_CLIENT_CERT_URL_FILE"
515   wait_for_file "$LFS_CERT_FILE"
516   wait_for_file "$LFS_CLIENT_CERT_FILE"
517   wait_for_file "$LFS_CLIENT_KEY_FILE"
518
519   LFS_CLIENT_CERT_URL=`cat $LFS_CLIENT_CERT_URL_FILE`
520
521   # Set up the initial git config and osx keychain if applicable
522   HOME="$TESTHOME"
523   if [ ! -d "$HOME" ]; then
524     mkdir "$HOME"
525   fi
526
527   if [ ! -f $HOME/.gitconfig ]; then
528     git lfs install --skip-repo
529     git config --global credential.usehttppath true
530     git config --global credential.helper lfstest
531     git config --global user.name "Git LFS Tests"
532     git config --global user.email "git-lfs@example.com"
533     git config --global http.sslcainfo "$LFS_CERT_FILE"
534     git config --global http.$LFS_CLIENT_CERT_URL/.sslKey "$LFS_CLIENT_KEY_FILE"
535     git config --global http.$LFS_CLIENT_CERT_URL/.sslCert "$LFS_CLIENT_CERT_FILE"
536     git config --global http.$LFS_CLIENT_CERT_URL/.sslVerify "false"
537   fi | sed -e 's/^/# /g'
538
539   # setup the git credential password storage
540   mkdir -p "$CREDSDIR"
541   printf "user:pass" > "$CREDSDIR/127.0.0.1"
542
543   echo "#"
544   echo "# HOME: $HOME"
545   echo "# TMP: $TMPDIR"
546   echo "# CREDS: $CREDSDIR"
547   echo "# lfstest-gitserver:"
548   echo "#   LFSTEST_URL=$LFS_URL_FILE"
549   echo "#   LFSTEST_SSL_URL=$LFS_SSL_URL_FILE"
550   echo "#   LFSTEST_CLIENT_CERT_URL=$LFS_CLIENT_CERT_URL_FILE ($LFS_CLIENT_CERT_URL)"
551   echo "#   LFSTEST_CERT=$LFS_CERT_FILE"
552   echo "#   LFSTEST_CLIENT_CERT=$LFS_CLIENT_CERT_FILE"
553   echo "#   LFSTEST_CLIENT_KEY=$LFS_CLIENT_KEY_FILE"
554   echo "#   LFSTEST_DIR=$REMOTEDIR"
555 }
556
557 # shutdown cleans the $TRASHDIR and shuts the test Git server down.
558 shutdown() {
559   # every t/t-*.sh file should cleanup its trashdir
560   [ -z "$KEEPTRASH" ] && rm -rf "$TRASHDIR"
561
562   if [ -z "$GIT_LFS_NO_TEST_COUNT" ]; then
563     LFSTEST_DIR="$REMOTEDIR" \
564     LFS_URL_FILE="$LFS_URL_FILE" \
565       lfstest-count-tests decrement
566   fi
567 }
568
569 tap_show_plan() {
570   local tests="$1"
571
572   printf "1..%i\n" "$tests"
573 }
574
575 ensure_git_version_isnt() {
576   local expectedComparison=$1
577   local version=$2
578
579   local gitVersion=$(git version | cut -d" " -f3)
580
581   set +e
582   compare_version $gitVersion $version
583   result=$?
584   set -e
585
586   if [[ $result == $expectedComparison ]]; then
587     echo "skip: $0 (git version $(comparison_to_operator $expectedComparison) $version)"
588     exit
589   fi
590 }
591
592 VERSION_EQUAL=0
593 VERSION_HIGHER=1
594 VERSION_LOWER=2
595
596 # Compare $1 and $2 and return VERSION_EQUAL / VERSION_LOWER / VERSION_HIGHER
597 compare_version() {
598     if [[ $1 == $2 ]]
599     then
600         return $VERSION_EQUAL
601     fi
602     local IFS=.
603     local i ver1=($1) ver2=($2)
604     # fill empty fields in ver1 with zeros
605     for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
606     do
607         ver1[i]=0
608     done
609     for ((i=0; i<${#ver1[@]}; i++))
610     do
611         if [[ -z ${ver2[i]} ]]
612         then
613             # fill empty fields in ver2 with zeros
614             ver2[i]=0
615         fi
616         if ((10#${ver1[i]} > 10#${ver2[i]}))
617         then
618             return $VERSION_HIGHER
619         fi
620         if ((10#${ver1[i]} < 10#${ver2[i]}))
621         then
622             return $VERSION_LOWER
623         fi
624     done
625     return $VERSION_EQUAL
626 }
627
628 comparison_to_operator() {
629   local comparison=$1
630   if [[ $1 == $VERSION_EQUAL ]]; then
631     echo "=="
632   elif [[ $1 == $VERSION_HIGHER ]]; then
633     echo ">"
634   elif [[ $1 == $VERSION_LOWER ]]; then
635     echo "<"
636   else
637     echo "???"
638   fi
639 }
640
641 # Calculate the object ID from the string passed as the argument
642 calc_oid() {
643   printf "$1" | $SHASUM | cut -f 1 -d " "
644 }
645
646 # Calculate the object ID from the file passed as the argument
647 calc_oid_file() {
648   $SHASUM "$1" | cut -f 1 -d " "
649 }
650
651 # Get a date string with an offset
652 # Args: One or more date offsets of the form (regex) "[+-]\d+[dmyHM]"
653 # e.g. +1d = 1 day forward from today
654 #      -5y = 5 years before today
655 # Example call:
656 #   D=$(get_date +1y +1m -5H)
657 # returns date as string in RFC3339 format ccyy-mm-ddThh:MM:ssZ
658 # note returns in UTC time not local time hence Z and not +/-
659 get_date() {
660   # Wrapped because BSD (inc OSX) & GNU 'date' functions are different
661   # on Windows under Git Bash it's GNU
662   if date --version >/dev/null 2>&1 ; then # GNU
663     ARGS=""
664     for var in "$@"
665     do
666         # GNU offsets are more verbose
667         unit=${var: -1}
668         val=${var:0:${#var}-1}
669         case "$unit" in
670           d) unit="days" ;;
671           m) unit="months" ;;
672           y) unit="years"  ;;
673           H) unit="hours"  ;;
674           M) unit="minutes" ;;
675         esac
676         ARGS="$ARGS $val $unit"
677     done
678     date -d "$ARGS" -u +%Y-%m-%dT%TZ
679   else # BSD
680     ARGS=""
681     for var in "$@"
682     do
683         ARGS="$ARGS -v$var"
684     done
685     date $ARGS -u +%Y-%m-%dT%TZ
686   fi
687 }
688
689 # Convert potentially MinGW bash paths to native Windows paths
690 # Needed to match generic built paths in test scripts to native paths generated from Go
691 native_path() {
692   local arg=$1
693   if [ $IS_WINDOWS -eq 1 ]; then
694     # Use params form to avoid interpreting any '\' characters
695     printf '%s' "$(cygpath -w $arg)"
696   else
697     printf '%s' "$arg"
698   fi
699 }
700
701 # escape any instance of '\' with '\\' on Windows
702 escape_path() {
703   local unescaped="$1"
704   if [ $IS_WINDOWS -eq 1 ]; then
705     printf '%s' "${unescaped//\\/\\\\}"
706   else
707     printf '%s' "$unescaped"
708   fi
709 }
710
711 # As native_path but escape all backslash characters to "\\"
712 native_path_escaped() {
713   local unescaped=$(native_path "$1")
714   escape_path "$unescaped"
715 }
716
717 # native_path_list_separator prints the operating system-specific path list
718 # separator.
719 native_path_list_separator() {
720   if [ "$IS_WINDOWS" -eq 1 ]; then
721     printf ";";
722   else
723     printf ":";
724   fi
725 }
726
727 cat_end() {
728   if [ $IS_WINDOWS -eq 1 ]; then
729     printf '^M$'
730   else
731     printf '$'
732   fi
733 }
734
735 # Compare 2 lists which are newline-delimited in a string, ignoring ordering and blank lines
736 contains_same_elements() {
737   # Remove blank lines then sort
738   diff -u <(printf '%s' "$1" | grep -v '^$' | sort) <(printf '%s' "$2" | grep -v '^$' | sort)
739 }
740
741 is_stdin_attached() {
742   test -t0
743   echo $?
744 }
745
746 has_test_dir() {
747   if [ -z "$GIT_LFS_TEST_DIR" ]; then
748     echo "No GIT_LFS_TEST_DIR. Skipping..."
749     exit 0
750   fi
751 }
752
753 add_symlink() {
754   local src=$1
755   local dest=$2
756
757   prefix=`git rev-parse --show-prefix`
758   hashsrc=`printf "$src" | git hash-object -w --stdin`
759
760   git update-index --add --cacheinfo 120000 "$hashsrc" "$prefix$dest"
761   git checkout -- "$dest"
762 }