Skip to contentSkip to conversation

git-fetch: Split fetch and merge logic

PATCH In review Santi Béjar 2 participants 6 messages Jun 15, 2016

lore
Share & tools
lore
#0 lore raw

PATCH v3 1/1 git-fetch: Split fetch and merge logic

It makes git-parse-remote.sh and almost all git-fetch independent of the merge logic.

git-fetch fetches the branches from the remote and saves this information in .git/FETCH_FETCHED, and at the end it generates the file .git/FETCH_HEAD.

The current merge behaviour is unchanged.

---
Hi *,

This version removes the behaviour changes.

Now to rebase this on top of 'jc/fetch' (a branch of 'pu').

Santi

 git-fetch.sh        |   79 +++++++++++++++++++++++++++++++--------------------
 git-parse-remote.sh |   64 ++++++++++++-----------------------------
 t/t5510-fetch.sh    |   16 ++++++++++
 3 files changed, 83 insertions(+), 76 deletions(-)
git-fetch.sh +48 -31
index d230995..637d732 100755
@@ -77,9 +77,9 @@ do
 	shift
 done
 
+origin=$(get_default_remote)
 case "$#" in
 0)
-	origin=$(get_default_remote)
 	test -n "$(get_remote_url ${origin})" ||
 		die "Where do you want to fetch from today?"
 	set x $origin ; shift ;;
@@ -101,6 +101,7 @@ if test "" = "$append"
 then
 	: >"$GIT_DIR/FETCH_HEAD"
 fi
+: >"$GIT_DIR/FETCH_FETCHED"
 
 # Global that is reused later
 ls_remote_result=$(git ls-remote $exec "$remote") ||
@@ -112,10 +113,6 @@ append_fetch_head () {
     remote_name_="$3"
     remote_nick_="$4"
     local_name_="$5"
-    case "$6" in
-    t) not_for_merge_='not-for-merge' ;;
-    '') not_for_merge_= ;;
-    esac
 
     # remote-nick is the URL given on the command line (or a shorthand)
     # remote-name is the $GIT_DIR relative refs/ path we computed
@@ -146,9 +143,7 @@ append_fetch_head () {
     if git-cat-file commit "$head_" >/dev/null 2>&1
     then
 	headc_=$(git-rev-parse --verify "$head_^0") || exit
-	echo "$headc_	$not_for_merge_	$note_" >>"$GIT_DIR/FETCH_HEAD"
-    else
-	echo "$head_	not-for-merge	$note_" >>"$GIT_DIR/FETCH_HEAD"
+	echo "$headc_	$remote_name_:$local_name_	$note_" >>"$GIT_DIR/FETCH_FETCHED"
     fi
 
     update_local_ref "$local_name_" "$head_" "$note_"
@@ -265,7 +260,7 @@ then
 		  git-show-ref --exclude-existing=refs/tags/ |
 	          while read sha1 name
 		  do
-			echo ".${name}:${name}"
+			echo "${name}:${name}"
 		  done` || exit
 	if test "$#" -gt 1
 	then
@@ -288,13 +283,6 @@ fetch_main () {
 
       # These are relative path from $GIT_DIR, typically starting at refs/
       # but may be HEAD
-      if expr "z$ref" : 'z\.' >/dev/null
-      then
-	  not_for_merge=t
-	  ref=$(expr "z$ref" : 'z\.\(.*\)')
-      else
-	  not_for_merge=
-      fi
       if expr "z$ref" : 'z+' >/dev/null
       then
 	  single_force=t
@@ -375,7 +363,7 @@ fetch_main () {
       esac
 
       append_fetch_head "$head" "$remote" \
-	  "$remote_name" "$remote_nick" "$local_name" "$not_for_merge" || exit
+	  "$remote_name" "$remote_nick" "$local_name" || exit
 
   done
 
@@ -418,28 +406,16 @@ fetch_main () {
 	      case "$ref" in
 	      +$remote_name:*)
 		  single_force=t
-		  not_for_merge=
-		  found="$ref"
-		  break ;;
-	      .+$remote_name:*)
-		  single_force=t
-		  not_for_merge=t
-		  found="$ref"
-		  break ;;
-	      .$remote_name:*)
-		  not_for_merge=t
 		  found="$ref"
 		  break ;;
 	      $remote_name:*)
-		  not_for_merge=
 		  found="$ref"
 		  break ;;
 	      esac
 	  done
 	  local_name=$(expr "z$found" : 'z[^:]*:\(.*\)')
 	  append_fetch_head "$sha1" "$remote" \
-		  "$remote_name" "$remote_nick" "$local_name" \
-		  "$not_for_merge" || exit
+		  "$remote_name" "$remote_nick" "$local_name" || exit
         done
       )
     ) || exit ;;
@@ -463,7 +439,7 @@ case "$no_tags$tags" in
 		do
 			git-cat-file -t "$sha1" >/dev/null 2>&1 || continue
 			echo >&2 "Auto-following $name"
-			echo ".${name}:${name}"
+			echo "${name}:${name}"
 		done)
 	esac
 	case "$taglist" in
@@ -491,3 +467,44 @@ case "$orig_head" in
 	fi
 	;;
 esac
+
+# Generate $GIT_DIR/FETCH_HEAD
+case ",$#,$remote_nick," in
+,1,$origin,)
+	curr_branch=$(git-symbolic-ref -q HEAD | sed -e 's|^refs/heads/||')
+	merge_branches=$(git-repo-config \
+		--get-all "branch.${curr_branch}.merge" | sort -u)
+	fetch_branches=$(get_remote_default_refs_for_fetch -n $remote_nick |
+		sed 's/:.*$//g;s/^+//' | sort -u)
+	test "$merge_branches" && test "$fetch_branches" &&
+	merge_branches=$(echo -e "$merge_branches\n$fetch_branches" | sort | uniq -d)
+	[ -z "$merge_branches" ] ||
+	test "$(get_data_source $remote_nick)" = branches && merge_first=yes;;
+,1,$remote,) merge_branches=HEAD;;
+,1,*)merge_first=yes ;;
+*)
+	shift
+	merge_branches=$(for ref; do
+		expr "z$ref" : 'z.*:' >/dev/null || ref="${ref}:"
+		echo "$(expr "z$ref" : 'z\([^:]*\):')"; done);;
+esac
+
+test "$merge_first" = "yes" &&
+test "$(get_remote_default_refs_for_fetch -t $remote_nick)" != "explicit" &&
+merge_branches= && merge_first=
+
+merge_branches=$(canon_refs_list_for_fetch $merge_branches | sed 's/:.*$//g')
+
+cat "$GIT_DIR"/FETCH_FETCHED | while IFS='	' read sha1 ref note ; do
+	remote_branch=$(expr "z$ref" : 'z\([^:]*\):')
+	for merge_branch in $merge_branches ; do
+		[ "$merge_branch" == "$remote_branch" ] &&
+			echo "$sha1		$note" && continue 2
+	done
+	if ! test "$merge_first" || test "$merge_first" == "done" ; then
+		echo "$sha1	not-for-merge	$note"
+	else
+		echo "$sha1		$note"
+		merge_first=done
+	fi
+done >> "$GIT_DIR/FETCH_HEAD"
git-parse-remote.sh +19 -45

107 lines not on this page · raw message

t/t5510-fetch.sh +16 -0
index 50c6485..0a19a7d 100755
@@ -50,6 +50,22 @@ test_expect_success "fetch test" '
 	test "z$mine" = "z$his"
 '
 
+test_expect_success "fetch test fetched" '
+	cd "$D" &&
+	cd three &&
+	git fetch &&
+	test -f .git/refs/heads/two &&
+	test -f .git/refs/heads/one &&
+	master_in_two=`cd ../two && git rev-parse master` &&
+	one_in_two=`cd ../two && git rev-parse one` &&
+	{
+		echo "$master_in_two	refs/heads/master:refs/heads/two"
+		echo "$one_in_two	refs/heads/one:refs/heads/one"
+	} >expected &&
+	cut -f -2 .git/FETCH_FETCHED >actual &&
+	diff expected actual'
+
+
 test_expect_success "fetch test for-merge" '
 	cd "$D" &&
 	cd three &&
Signature · 1 line
1.5.0.1.576.g78541
#1 lore raw
Quoted 8 lines · Santi Béjar

It makes git-parse-remote.sh and almost all git-fetch independent of the merge logic.

git-fetch fetches the branches from the remote and saves this information in .git/FETCH_FETCHED, and at the end it generates the file .git/FETCH_HEAD.

The current merge behaviour is unchanged.

I overlooked your earlier huge testcase patch, but I noticed it does not include any test that fetches an annotated tag that points at a commit or a non-commit, nor an unannotated tag that is a tree or a blob. I am too tired to check them right now, but are you sure you are not regressing with them?

It is sometimes handy to be able to say:

	$ git fetch git-gui tag spearce-gpg-pub
        $ git cat-file -p FETCH_HEAD
#2 lore raw
Quoted 8 lines · Junio C Hamano

Santi Béjar <[email protected]> writes:
...

Quoted 1 line

The current merge behaviour is unchanged.

I overlooked your earlier huge testcase patch, but I noticed it does not include any test that fetches an annotated tag that points at a commit or a non-commit, nor an unannotated tag that is a tree or a blob.

A couple of other things I think the users of information in FETCH_HEAD rely on need to be checked by the test suite.

* Automated following of tags when a branch head is fetched. When this happens, I think the users (not just git-pull script but end users) expect the branch head to be listed first in the FETCH_HEAD file, so that:

	$ git fetch git://some.repo/
	$ git show FETCH_HEAD

gives stable and useful result.

* When two refs are listed on the command line explicitly, are they listed in the order given by the user in the resulting FETCH_HEAD file?

	$ git fetch git://some.repo/ one two
	$ git fetch git://some.repo/ two one

Again, "git show FETCH_HEAD" after that expects the first one to show 'one' and the second one 'two'. Replacing 'fetch' with 'pull' in the example would affect the parent order in the resulting Octopus.

#3 lore raw
In reply to Santi Béjar
Quoted 6 lines · Santi Béjar

It makes git-parse-remote.sh and almost all git-fetch independent of the merge logic.

git-fetch fetches the branches from the remote and saves this information in .git/FETCH_FETCHED, and at the end it generates the file .git/FETCH_HEAD.

I might have some more comments after actually applying this and reviewing it with wider contexts, but it looks nice overall.

I am wondering if FETCH_FETCHED is purely for internal use by git-fetch (it appears so), and if so if it is worth trying to do without the temporary file, but that is a minor detail.

Quoted 4 lines
 git-fetch.sh        |   79 +++++++++++++++++++++++++++++++--------------------
 git-parse-remote.sh |   64 ++++++++++++-----------------------------
 t/t5510-fetch.sh    |   16 ++++++++++
 3 files changed, 83 insertions(+), 76 deletions(-)

Loses more lines than it adds (+16 lines to test does not count), which is a very good sign.

Quoted 5 lines
git-fetch.sh +0 -0
index d230995..637d732 100755

git-fetch.sh case "$orig_head" in · line 476

Quoted 10 lines
git-fetch.sh +7 -0
@@ -491,3 +467,44 @@ case "$orig_head" in
 	fi
 	;;
 esac
+
+# Generate $GIT_DIR/FETCH_HEAD
+case ",$#,$remote_nick," in
+,1,$origin,)
+	curr_branch=$(git-symbolic-ref -q HEAD | sed -e 's|^refs/heads/||')
+	merge_branches=$(git-repo-config \
+		--get-all "branch.${curr_branch}.merge" | sort -u)

Why "sort -u" (instead of erroring out if the repository is misconfigured)?

git-fetch.sh case "$orig_head" in · line 478

Quoted 2 lines
git-fetch.sh +2 -0
+	fetch_branches=$(get_remote_default_refs_for_fetch -n $remote_nick |
+		sed 's/:.*$//g;s/^+//' | sort -u)

GNU sed users do not have problems with this, but I recall that we had to rewrite our sed scripts not to use multiple commands concatenated with ';' for portability, like:

	sed -e 's/:.*$//g' -e 's/^+//'

Again why "sort -u"?

git-fetch.sh case "$orig_head" in · line 479

Quoted 1 line
git-fetch.sh +1 -0
+	test "$merge_branches" && test "$fetch_branches" &&

We probably would want to be defensive by saying "test -n".

git-fetch.sh case "$orig_head" in · line 480

Quoted 1 line
git-fetch.sh +1 -0
+	merge_branches=$(echo -e "$merge_branches\n$fetch_branches" | sort | uniq -d)

I appreciate the cleverness of the intersection. However, is "echo -e" portable? I think we have avoided it so far (we have avoided even "echo -n" which is traditionally much more available).

git-fetch.sh case "$orig_head" in · line 510

Quoted 13 lines
git-fetch.sh +13 -0
+cat "$GIT_DIR"/FETCH_FETCHED | while IFS='	' read sha1 ref note ; do
+	remote_branch=$(expr "z$ref" : 'z\([^:]*\):')
+	for merge_branch in $merge_branches ; do
+		[ "$merge_branch" == "$remote_branch" ] &&
+			echo "$sha1		$note" && continue 2
+	done
+	if ! test "$merge_first" || test "$merge_first" == "done" ; then
+		echo "$sha1	not-for-merge	$note"
+	else
+		echo "$sha1		$note"
+		merge_first=done
+	fi
+done >> "$GIT_DIR/FETCH_HEAD"

You can do:

	while ...
        do
        done < "$GIT_DIR/FETCH_FETCHED"

which is easier on the eye.

I often see a buggy shell script that expects assignment in a while loop to survive after the loop finished, when the loop is on the downstream side of a pipe (e.g. the loop is run in a subshell so merge_first after this loop is finished will never be 'done'). You do not use the variable after the loop so your script is not buggy, but avoiding a pipe into while loop is a good habit to get into.

git-parse-remote.sh canon_refs_list_for_fetch () { · line 162

Quoted 11 lines
git-parse-remote.sh +2 -2
index 5208ee6..691d46c 100755
@@ -196,32 +159,43 @@ canon_refs_list_for_fetch () {
 
 	config)
-		canon_refs_list_for_fetch -d "$1" \
-			$(git-config --get-all "remote.$1.fetch") ;;
+		set $(expand_refs_wildcard "$1" \
+			$(git-repo-config --get-all "remote.$1.fetch")) ;;

Oops? It is not buggy but it's better to set an example by using git-config consistenty. You have another mention of repo-config above.

git-parse-remote.sh canon_refs_list_for_fetch () { · line 185

Quoted 6 lines
git-parse-remote.sh +2 -2
 	remotes)
-		canon_refs_list_for_fetch -d "$1" $(sed -ne '/^Pull: */{
+		set $(expand_refs_wildcard "$1" $(sed -ne '/^Pull: */{
 						s///p
-					}' "$GIT_DIR/remotes/$1")
+					}' "$GIT_DIR/remotes/$1"))

Hmph. I wonder why the original author did not do '/^Pull: */s///p'...

#4 lore raw
Quoted 11 lines · Junio C Hamano
Quoted 6 lines · Santi Béjar

It makes git-parse-remote.sh and almost all git-fetch independent of the merge logic.

git-fetch fetches the branches from the remote and saves this information in .git/FETCH_FETCHED, and at the end it generates the file .git/FETCH_HEAD.

I might have some more comments after actually applying this and reviewing it with wider contexts, but it looks nice overall.

Thanks.

Quoted 4 lines

I am wondering if FETCH_FETCHED is purely for internal use by git-fetch (it appears so), and if so if it is worth trying to do without the temporary file, but that is a minor detail.

Yes, it's purely internal. With "without the temporary file" you mean to put the content in a variable or removing at the end?

Quoted 10 lines
Quoted 10 lines
git-fetch.sh +2 -0
index d230995..637d732 100755
@@ -491,3 +467,44 @@ case "$orig_head" in
      fi
      ;;
 esac
+
+# Generate $GIT_DIR/FETCH_HEAD
Quoted 8 lines

git-fetch.sh case "$orig_head" in · line 476

Quoted 5 lines
git-fetch.sh +5 -0
+case ",$#,$remote_nick," in
+,1,$origin,)
+     curr_branch=$(git-symbolic-ref -q HEAD | sed -e 's|^refs/heads/||')
+     merge_branches=$(git-repo-config \
+             --get-all "branch.${curr_branch}.merge" | sort -u)

Why "sort -u" (instead of erroring out if the repository is misconfigured)?

It is consistent with the current code and I needed it for the intersection below. The check could be added later.

Quoted 9 lines

git-fetch.sh case "$orig_head" in · line 478

Quoted 3 lines
git-fetch.sh +2 -0
+     fetch_branches=$(get_remote_default_refs_for_fetch -n $remote_nick |
+             sed 's/:.*$//g;s/^+//' | sort -u)

GNU sed users do not have problems with this, but I recall that we had to rewrite our sed scripts not to use multiple commands concatenated with ';' for portability, like:

        sed -e 's/:.*$//g' -e 's/^+//'

OK.

Quoted 2 lines

Again why "sort -u"?

For the intersection below. Moreover, it make sense because remotes.${remote}.fetch could be of the form:

[remote "origin"]
url=...
fetch= refs/heads/master:refs/remotes/origin/master fetch= refs/heads/*:refs/remotes/origin/*

to have a well defined first refspec. In this case refs/heads/master appears twice.

Quoted 5 lines

git-fetch.sh case "$orig_head" in · line 479

Quoted 2 lines
git-fetch.sh +1 -0
+     test "$merge_branches" && test "$fetch_branches" &&

We probably would want to be defensive by saying "test -n".

Ok.

Quoted 6 lines

git-fetch.sh case "$orig_head" in · line 480

Quoted 1 line
git-fetch.sh +1 -0
+     merge_branches=$(echo -e "$merge_branches\n$fetch_branches" | sort | uniq -d)

I appreciate the cleverness of the intersection. However, is "echo -e" portable? I think we have avoided it so far (we have avoided even "echo -n" which is traditionally much more available).

printf '%s\n%s' "$merge_branches" "$fetch_branches"

is OK?

Quoted 23 lines
Quoted 4 lines
git-fetch.sh +3 -0
+cat "$GIT_DIR"/FETCH_FETCHED | while IFS='   ' read sha1 ref note ; do
+     remote_branch=$(expr "z$ref" : 'z\([^:]*\):')
+     for merge_branch in $merge_branches ; do

git-fetch.sh case "$orig_head" in · line 510

Quoted 10 lines
git-fetch.sh +10 -0
+             [ "$merge_branch" == "$remote_branch" ] &&
+                     echo "$sha1             $note" && continue 2
+     done
+     if ! test "$merge_first" || test "$merge_first" == "done" ; then
+             echo "$sha1     not-for-merge   $note"
+     else
+             echo "$sha1             $note"
+             merge_first=done
+     fi
+done >> "$GIT_DIR/FETCH_HEAD"

You can do:

while ...
do
done < "$GIT_DIR/FETCH_FETCHED"

which is easier on the eye.

Quoted 7 lines

I often see a buggy shell script that expects assignment in a while loop to survive after the loop finished, when the loop is on the downstream side of a pipe (e.g. the loop is run in a subshell so merge_first after this loop is finished will never be 'done'). You do not use the variable after the loop so your script is not buggy, but avoiding a pipe into while loop is a good habit to get into.

OK, and thanks for the explanation.

Quoted 13 lines

git-parse-remote.sh canon_refs_list_for_fetch () { · line 162

Quoted 12 lines
git-parse-remote.sh +2 -2
index 5208ee6..691d46c 100755
@@ -196,32 +159,43 @@ canon_refs_list_for_fetch () {
 
      config)
-             canon_refs_list_for_fetch -d "$1" \
-                     $(git-config --get-all "remote.$1.fetch") ;;
+             set $(expand_refs_wildcard "$1" \
+                     $(git-repo-config --get-all "remote.$1.fetch")) ;;
Quoted 3 lines

Oops? It is not buggy but it's better to set an example by using git-config consistenty. You have another mention of repo-config above.

Yes, of course.

Quoted 9 lines

git-parse-remote.sh canon_refs_list_for_fetch () { · line 185

Quoted 7 lines
git-parse-remote.sh +2 -2
      remotes)
-             canon_refs_list_for_fetch -d "$1" $(sed -ne '/^Pull: */{
+             set $(expand_refs_wildcard "$1" $(sed -ne '/^Pull: */{
                                              s///p
-                                     }' "$GIT_DIR/remotes/$1")
+                                     }' "$GIT_DIR/remotes/$1"))

Hmph. I wonder why the original author did not do '/^Pull: */s///p'...

I don't know, I'll do.

Santi

#5 lore raw
Quoted 8 lines · Santi Béjar
Quoted 3 lines · Junio C Hamano

I am wondering if FETCH_FETCHED is purely for internal use by git-fetch (it appears so), and if so if it is worth trying to do without the temporary file, but that is a minor detail.

Yes, it's purely internal. With "without the temporary file" you mean to put the content in a variable or removing at the end?

If a variable suffices that would be quite nice, but it is not a big deal. As the script does fair amount of computation in subprocess, I suspect it may not be worth trying to use variable, only to get rid of the temporary file.

Quoted 8 lines
Quoted 4 lines

I appreciate the cleverness of the intersection. However, is "echo -e" portable? I think we have avoided it so far (we have avoided even "echo -n" which is traditionally much more available).

printf '%s\n%s' "$merge_branches" "$fetch_branches"

is OK?

Yes. I think printf is what people who rewrote my "echo -n" have done elsewhere.