#!/usr/bin/env bash # # Fetch changes from a remote branch to the local GIT repository. # Copyright (c) Petr Baudis, 2005. # # Takes the branch name as an argument, defaulting to 'origin'. # # This will fetch the latest changes from a remote repository to the # corresponding branch in your local repository. Note that this operation # does not involve merging those changes to your own branch - that is being # done by the `cg-merge` command. `cg-update` exists to conveniently bundle # the act of fetching and merging to your working branch together. # # Before the first fetch, you have to tell Cogito about the remote branch. # This should be done by the `cg-branch-add` command. See its documentation # for the list of supported fetching protocols and other details. Note that # one exception to this is the 'origin' branch, which was set to the location # of the source repository if you created yours using the `cg-clone` command. # # Note that the operation now being performed by `cg-fetch` ('fetching') # was called 'pulling' in the past. However, GIT recently changed this # terminology, and after sufficient transition period, the 'pulling' # expression will be instead used for the operation now performed by the # `cg-update` command. Please do not let this confuse you. # # OPTIONS # ------- # -f:: Force the complete fetch even if the heads are the same. # Force the complete fetch even if the heads are the same. # # -v:: Enable verbosity # Display more verbose output - most notably list all the files # touched by the fetched changes. Use twice to get even more verbosity, # that is raw progress information instead of the progress bar. # # ENVIRONMENT # ----------- # RSYNC:: # The command to invoke when we want to call the rsync tool (only used # when fetching over the rsync protocol). Defaults to 'rsync'. # # RSYNC_FLAGS:: # Additional flags to be passed to the rsync tool when fetching over # the rsync protocol. USAGE="cg-fetch [-f] [-v] [BRANCH_NAME]" . "${COGITO_LIB}"cg-Xlib || exit 1 deprecated_alias cg-fetch cg-pull fetch_progress() { [ $verbose -ge 2 ] && exec cat if [ -t 1 ]; then exec "${COGITO_LIB}"cg-Xfetchprogress "$_git_objects" else exec cat >/dev/null fi } show_changes_summary() { local orig_head="$1" local new_head="$2" if [ ! "$orig_head" ]; then echo "New branch: $new_head" elif [ "$orig_head" != "$new_head" ]; then echo "Tree change: $orig_head:$new_head" [ $verbose -ge 1 ] && git-diff-tree --abbrev -r "$(cg-object-id -t "$orig_head")" "$(cg-object-id -t "$new_head")" else echo "Up to date." fi } initial_done() { rm -f ___ cp "$_git/refs/heads/origin" "$_git/refs/heads/master" && git-read-tree HEAD && git-checkout-index -a && git-update-index --refresh || die "initial checkout failed" rm "$_git/info/cg-fetch-initial" } get_rsync() { [ "$1" = "-b" ] && shift redir= if [ "$1" = "-i" ]; then # ignore-errors redir="2>/dev/null" shift fi filter="cat" if [ "$1" = "-s" ]; then # subsequent # We already saw the MOTD, thank you very much. filter="grep -v ^MOTD:" shift fi appenduri= if [ "$1" = "-d" ]; then # directory appenduri="/." # CowboyNeal shift fi echo "${RSYNC:-rsync}" $RSYNC_FLAGS -v --partial -Lr \ "$1$appenduri" "$2$appenduri" $redir eval '"${RSYNC:-rsync}"' $RSYNC_FLAGS -v --partial -Lr \ '"$1$appenduri"' '"$2$appenduri"' $redir | $filter return ${PIPESTATUS[0]} } fetch_rsync() { if [ $verbose -ge 2 ]; then # We must not pipe to prevent buffered I/O get_rsync -s -d "$2/objects" "$_git_objects" else get_rsync -s -d "$2/objects" "$_git_objects" | fetch_progress fi ret=${PIPESTATUS[0]} if [ "$3" ] && [ "$ret" -eq "0" ]; then if [ "$orig_head" ]; then git-rev-list --objects $new_head ^$orig_head | while read obj type; do git-cat-file -t $obj >/dev/null || exit $? done || die "rsync fetch incomplete, some objects missing" fi cat "$_git/refs/${3%/*}/.${3##*/}-fetching" > "$_git/refs/$3" fi return $ret } get_http() { [ "$1" = "-b" ] && shift [ "$1" = "-i" ] && shift [ "$1" = "-s" ] && shift [ "$1" = "-d" ] && die "INTERNAL ERROR: HTTP recursive not implemented" src="$1" dest="$2" curl_extra_args= [ "$GIT_SSL_NO_VERIFY" ] && curl_extra_args="-k" curl -nsfL $curl_extra_args -o "$dest" "$src" } fetch_http() { whead= [ "$3" ] && whead="-w $3" (git-http-fetch -a -v $whead $recovery "$1" "$2/" 2>&1 /dev/null) | fetch_progress return ${PIPESTATUS[0]} } get_local() { cp_flags_l="-v" if [ "$1" = "-b" ]; then # Dereference symlinks cp_flags_l="$cp_flags_l -L" shift else cp_flags_l="$cp_flags_l -pRP" fi [ "$1" = "-i" ] && shift [ "$1" = "-s" ] && shift [ "$1" = "-d" ] && die "INTERNAL ERROR: local-fetch recursive not implemented" src="$1" dest="$2" cp $cp_flags_l "$src" "$dest" } fetch_local() { whead= [ "$3" ] && whead="-w $3" (git-local-fetch -a -l -v $whead $recovery "$1" "$2" 2>&1 /dev/null) | fetch_progress return ${PIPESTATUS[0]} } fetch_tags() { echo "Fetching tags..." # FIXME: Warn about conflicting tag names? [ -d "$_git/refs/tags" ] || mkdir -p "$_git/refs/tags" if [ "$get" = "get_rsync" ]; then if ! $get -i -s -d "$uri/refs/tags" "$_git/refs/tags"; then echo "unable to get tags list (non-fatal)" >&2 return $? fi fi git-ls-remote --tags "$uri" | # SHA1 refs/tags/v0.99.8^{} --> SHA1 tags/v0.99.8 # where SHA1 is the object v0.99.8 tag points at. sed -ne 's:\([^ ]\) refs/\(tags/.*\)^{}$:\1 \2:p' | while read sha1 tagname; do # Do we have the tag itself? [ -s "$_git/refs/$tagname" ] && continue # Do we have the object pointed at by the tag? git-cat-file -t "$sha1" >/dev/null 2>&1 || continue # if so, fetch the tag -- which should be # a cheap operation -- to complete the chain. echo -n "Missing tag ${tagname#tags/}... " if $fetch "$tagname" "$uri" "$tagname" 2>/dev/null >&2; then echo "retrieved" else # 17 is code from packed transport, which # will grab all of them en masse later if [ "$?" -ne "17" ]; then echo "unable to retrieve" else echo "" fi fi done [ "${PIPESTATUS[0]}" -eq "0" ] || echo "unable to get tags list (non-fatal)" >&2 return 0 } recovery= verbose=0 while optparse; do if optparse -f; then # When forcing, let the fetch tools make more extensive # walk over the dependency tree with --recover. recovery=--recover elif optparse -v; then verbose=$((verbose+1)) else optfail fi done name="${ARGS[0]}" [ "$name" ] || { [ -s "$_git/branches/origin" ] && name=origin; } [ "$name" ] || die "where to fetch from?" uri=$(cat "$_git/branches/$name" 2>/dev/null) || die "unknown branch: $name" rembranch= if echo "$uri" | grep -q '#'; then rembranch=$(echo "$uri" | cut -d '#' -f 2) uri=$(echo "$uri" | cut -d '#' -f 1) fi # Some other process with the same pid might appear, that's why # we won't die but rather let the user check quickly. dirtyfile="$_git/info/cg-fetch-$(echo "$name" | sed -e 's/\//-.-/g')-dirty" if [ -s "$dirtyfile" ]; then kill -0 $(cat "$dirtyfile") 2>/dev/null && \ warn "aren't you fetching $name twice at once? (waiting 10s)" && \ sleep 10 if [ -s "$_git/info/cg-fetch-initial" ]; then echo "Recovering from a previously interrupted initial clone..." else echo "Recovering from a previously interrupted fetch..." fi recovery=--recover fi mkdir -p "$_git/info" echo $$ > "$dirtyfile" orig_head= [ -s "$_git/refs/heads/$name" ] && orig_head="$(cat "$_git/refs/heads/$name")" packed_transport= if echo "$uri" | grep -q "^https\?://"; then get=get_http fetch=fetch_http elif echo "$uri" | grep -q "^git+ssh://"; then packed_transport=ssh elif echo "$uri" | grep -q "^git://"; then packed_transport=git elif echo "$uri" | grep -q "^rsync://"; then echo "WARNING: The rsync access method is DEPRECATED and will be REMOVED in the future!" >&2 get=get_rsync fetch=fetch_rsync elif echo "$uri" | grep -q ":"; then echo "WARNING: I guessed the host:path syntax was used and fell back to the git+ssh protocol." >&2 echo "WARNING: The host:path syntax is evil because it is implicit. Please just use a URI." >&2 packed_transport=ssh else [ -d "$uri/.git" ] && uri="$uri/.git" [ -d "$uri" ] || die "repository not found" get=get_local fetch=fetch_local # Perhaps the object database is shared symlinked= is_same_repo "$_git_objects" "$uri/objects" && symlinked=1 # See if we can hardlink and add "-l" to cp flags. can_hardlink= sample_file="$(find "$uri" -type f -print | head -n 1)" rm -f "$_git/.,,lntest" if cp -fl "$sample_file" "$_git/.,,lntest" 2>/dev/null; then can_hardlink=l echo "Using hard links" else echo "Hard links don't work - using copy" fi rm -f "$_git/.,,lntest" fi if [ "$packed_transport" ]; then # This is a really special case. [ "$rembranch" ] || rembranch="HEAD" cloneorfetch= #fetch [ -s "$_git/info/cg-fetch-initial" ] && cloneorfetch=-k #clone rm -f "$_git/info/cg-fetch-earlydie" fetch_pack_recorder () { while read sha1 remote_name; do [ "$sha1" = "failed" ] && die "$2" ref="$1"; [ "$ref" ] || ref="$remote_name" mkdir -p "$_git/$(dirname "$ref")" echo "$sha1" >"$_git/$ref" done } echo "Fetching pack (head and objects)..." ( git-fetch-pack $cloneorfetch "$uri" "$rembranch" || echo "failed" "$rembranch" ) | fetch_pack_recorder "refs/heads/$name" "fetching pack failed" || exit export _cg_taglist="$(mktemp -t gitfetch.XXXXXX)" record_tags_to_fetch () { echo "refs/$1" >>"$_cg_taglist" return 17 } fetch=record_tags_to_fetch fetch_tags if [ -s "$_cg_taglist" ]; then ( cat "$_cg_taglist" | tr '\n' '\0' | xargs -0 git-fetch-pack $cloneorfetch "$uri" || echo "failed" "$rembranch" ) | fetch_pack_recorder "" "unable to retrieve tags (non-fatal)" fi rm "$_cg_taglist" rm "$dirtyfile" show_changes_summary "$orig_head" "$(cg-object-id "$name")" [ -s "$_git/info/cg-fetch-initial" ] && initial_done exit 0 fi ### Behold, the fetch itself ## Grab the head echo "Fetching head..." mkdir -p "$_git/refs/heads/$(dirname "$name")" tmpname="$(dirname "$name")/.$(basename "$name")-fetching" if [ "$rembranch" ]; then $get -i "$uri/refs/heads/$rembranch" "$_git/refs/heads/$tmpname" || die "unable to get the head pointer of branch $rembranch" else $get -b "$uri/HEAD" "$_git/refs/heads/$tmpname" || die "unable to get the HEAD branch" fi new_head="$(cat "$_git/refs/heads/$tmpname")" if [ "${new_head#ref:}" != "$new_head" ]; then new_head="$(echo "$new_head" | sed 's/^ref: *//')" $get -i "$uri/$new_head" "$_git/refs/heads/$tmpname" || die "unable to get the head pointer of branch $new_head (referenced by HEAD)" new_head="$(cat "$_git/refs/heads/$tmpname")" fi rm -f "$_git/info/cg-fetch-earlydie" echo "Fetching objects..." ## Fetch the objects if ! [ "$symlinked" ]; then if [ "$recovery" -o "$orig_head" != "$new_head" ]; then [ -d "$_git_objects" ] || mkdir -p "$_git_objects" $fetch "$(cat "$_git/refs/heads/$tmpname")" "$uri" "heads/$name" || die "objects fetch failed" fi else cat "$_git/refs/heads/$tmpname" > "$_git/refs/heads/$name" fi rm "$_git/refs/heads/$tmpname" ## Fetch the tags ret=0 if ! fetch_tags; then ret=$? fi rm "$dirtyfile" show_changes_summary "$orig_head" "$new_head" [ -s "$_git/info/cg-fetch-initial" ] && initial_done exit $ret