Multi-head pulling series

5 messages, 2 authors, 2016-06-15 · open the first message on its own page

Multi-head pulling series

From: Junio C Hamano <hidden>
Date: 2016-06-15 22:42:04

Here is the current status of the multi-head download support.

    [PATCH 1/3] Start adding the $GIT_DIR/remotes/ support.
    [PATCH 2/3] Multi-head fetch.
    [PATCH 3/3] Update git-pull to match updated git-fetch.

The first one in the series adds support for the long promised
$GIT_DIR/remotes/ files; they use the following format:

	$ cat $GIT_DIR/remotes/www
	URL: http://www.kernel.org/pub/scm/git/git.git/
	Pull: master:ko-master pu:ko-pu

	$ cat $GIT_DIR/remotes/mko
	URL: master.kernel.org:/pub/scm/git/git.git/
	Pull: master:ko-master pu:ko-pu
	Push: master:master pu:pu foo:bar

The idea is that you should be able to say:

	$ git push mko ;# push our master and pu to the same
			# name, foo to .git/refs/heads/bar.
        
	$ git push mko pu:refs/heads/testing
			# instead of pushing to the usual ref,
                        # push our pu to refs/heads/testing,
                        # this time only. 
        
	$ git fetch www ;# get their master and pu to our ko-master
 			 # and ko-pu.

	$ git pull www bar
 			# instead of fetching from usual ref,
                        # fetch from there bar, and merge with it.

Having a Push: entry that uses anything but hostname:/path or
just local directory as its URL does not make any sense at this
moment, because git native send-pack transport is the only one
that is supported for push, as before.

Note that using a $GIT_DIR/branches/ file as a short-cut is
still supported, for both upload and download.

The second one extends "git fetch" to allow pulling from more
than one head.  Earlier, "git fetch <remote>" with shorthand
form (i.e. a file $GIT_DIR/branches/<remote> exists and
describes the URL) always overwrote
$GIT_DIR/refs/heads/<remote>, but it does the "reverse push",
meaning it does not overwrite when the update does not result in
a fast-forward merge with the existing value of the reference.
The above "git fetch www" example would fetch both their master
and pu, and tries to fast forward our ko-master and ko-pu with
them.  When fast forwarding fails, currently it just stops and
does not try to download all the heads.  This may need to
change.  The fetch results, whether they update the local
refs/heads/ hierarchy by fast forwarding or not, will be also
stored in $GIT_DIR/FETCH_HEAD.

Note that the format of this file has changed to accomodate
multiple heads.  I've checked that neither Cogito nor StGIT
looks at this file, so this change should hopefully be a safe
one.

The third one is to update the "git pull" to prevent people from
abusing the extended "git fetch" and attempt to create a king
ghidorah.  "git resolve" has not been updated to do anything but
two-head merge yet, so when you say "git pull www master pu rc",
it would run "git fetch www master pu rc" but stops without
running the resolving step.  While it would not make much sense
xsto do an octopus merge against master, pu and rc from git.git
repository, doing an octopus would probably make a lot more
sense when pulling from a repository like what Jeff Garzik has,
so that would be an interesting future project.

I have tested the very basic form of the above works reasonably
well, but there probably are bugs, maybe attempting to pull
heads to a wrong local heads, or something silly like that.
Please handle it with care.  It will stay in "pu" branch for
now.

[PATCH 2/3] Multi-head fetch.

From: Junio C Hamano <hidden>
Date: 2016-06-15 22:42:04

Traditionally, fetch takes these forms:

    $ git fetch <remote>
    $ git fetch <remote> <head>
    $ git fetch <remote> tag <tag>

This patch updates it to take

    $ git fetch <remote> <refspec>...

where:

    - A <refspec> of form "<src>:<dst>" is to fetch the objects
      needed for the remote ref that matches <src>, and if <dst>
      is not empty, store it as a local <dst>.

    - "tag" followed by <next> is just an old way of saying
      "refs/tags/<next>:refs/tags/<next>"; this mimics the
      current behaviour of the third form above and means "fetch
      that tag and store it under the same name".

    - A single token <refspec> without colon is a shorthand for
      "<refspec>:"  That is, "fetch that ref but do not store
      anywhere".

    - when there is no <refspec> specified

      - if <remote> is the name of a file under $GIT_DIR/remotes/
	(i.e. a new-style shorthand), then it is the same as giving
	the <refspec>s listed on Pull: line in that file.

      - if <remote> is the name of a file under $GIT_DIR/branches/
	(i.e. an old-style shorthand, without trailing path), then it
	is the same as giving a single <refspec>
	"<remote-name>:refs/heads/<remote>" on the command line, where
	<remote-name> is the remote branch name (defaults to HEAD, but
	can be overridden by .git/branches/<remote> file having the
	URL fragment notation).  That is, "fetch that branch head and
	store it in refs/heads/<remote>".

      - otherwise, it is the same as giving a single <refspec>
        that is "HEAD:".

The SHA1 object names of fetched refs are stored in FETCH_HEAD,
one name per line.

Signed-off-by: Junio C Hamano <redacted>
---

 git-fetch-script |  154 ++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 108 insertions(+), 46 deletions(-)

7dbfeb149dfc049015f27f15dd9135fd15d5f99f
diff --git a/git-fetch-script b/git-fetch-script
--- a/git-fetch-script
+++ b/git-fetch-script
@@ -1,54 +1,116 @@
 #!/bin/sh
 #
 . git-sh-setup-script || die "Not a git archive"
-. git-parse-remote "$@"
-merge_repo="$_remote_repo"
-merge_head="$_remote_head"
-merge_store="$_remote_store"
-
-TMP_HEAD="$GIT_DIR/TMP_HEAD"
-
-case "$merge_repo" in
-http://* | https://*)
-        if [ -n "$GIT_SSL_NO_VERIFY" ]; then
-            curl_extra_args="-k"
-        fi
-	_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' &&
-	_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40" &&
-	head=$(curl -nsf $curl_extra_args "$merge_repo/$merge_head") &&
-	expr "$head" : "$_x40\$" >/dev/null || {
-		echo >&2 "Failed to fetch $merge_head from $merge_repo"
-	        exit 1
-	}
-	echo Fetching "$merge_head" using http
-	git-http-pull -v -a "$head" "$merge_repo/" || exit
-	;;
-rsync://*)
-	rsync -L "$merge_repo/$merge_head" "$TMP_HEAD" || exit 1
-	head=$(git-rev-parse TMP_HEAD)
-	rm -f "$TMP_HEAD"
-	rsync -avz --ignore-existing "$merge_repo/objects/" "$GIT_OBJECT_DIRECTORY/"
-	;;
-*)
-	head=$(git-fetch-pack "$merge_repo" "$merge_head")
-	if h=`expr "$head" : '\([^ ][^ ]*\) '`
+. git-parse-remote-script
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+
+remote_nick="$1"
+remote=$(get_remote_url "$@")
+refs=
+rref=
+prev_was_literal_tag=
+rsync_slurped_objects=
+: >$GIT_DIR/FETCH_HEAD
+
+fast_forward_local () {
+
+	# NEEDSWORK: probably use the same cmpxchg protocol here...
+	echo "$2" >"$GIT_DIR/$1.lock"
+	if test -f "$GIT_DIR/$1"
 	then
-	    head=$h
+		local=$(git-rev-parse --verify "$1^0") &&
+		mb=$(git-merge-base "$local" "$2") &&
+		test "$mb" = "$local" || {
+			rm -f "$GIT_DIR/$1.lock"
+			die "$1 does not fast forward to $4 from $3.";
+		}
 	fi
-	;;
-esac || exit 1
-
-git-rev-parse --verify "$head" > /dev/null || exit 1
+	mv "$GIT_DIR/$1.lock" "$GIT_DIR/$1"
+}
 
-case "$merge_store" in
-'')
-	;;
-*)
-	echo "$head" > "$GIT_DIR/$merge_store"
-esac &&
+for ref in $(get_remote_refs_for_fetch "$@")
+do
+	if test "$prev_was_literal_tag"
+	then
+		ref="refs/tags/${ref}:refs/tags/${ref}"
+		prev_was_literal_tag=
+	else
+		case "$ref" in
+		tag)
+			prev_was_literal_tag=yes
+			continue
+			;;
+		esac
+	fi
 
-# FETCH_HEAD is fed to git-resolve-script which will eventually be
-# passed to git-commit-tree as one of the parents.  Make sure we do
-# not give a tag object ID.
+	refs="$refs $ref"
 
-git-rev-parse "$head^0" >"$GIT_DIR/FETCH_HEAD"
+	# These are relative path from $GIT_DIR, typically starting at refs/
+	# but may be HEAD
+	remote_name=$(expr "$ref" : '\([^:]*\):')
+	local_name=$(expr "$ref" : '[^:]*:\(.*\)')
+
+	rref="$rref $remote_name"
+
+	# There are transports that can fetch only one head at a time...
+	case "$remote" in
+	http://* | https://*)
+		if [ -n "$GIT_SSL_NO_VERIFY" ]; then
+		    curl_extra_args="-k"
+		fi
+		head=$(curl -nsf $curl_extra_args "$remote/$remote_name") &&
+		expr "$head" : "$_x40\$" >/dev/null ||
+			die "Failed to fetch $remote_name from $remote"
+		echo Fetching "$remote_name from $remote" using http
+		git-http-pull -v -a "$head" "$remote/" || exit
+		;;
+	rsync://*)
+		TMP_HEAD="$GIT_DIR/TMP_HEAD"
+		rsync -L "$remote/$remote_name" "$TMP_HEAD" || exit 1
+		head=$(git-rev-parse TMP_HEAD)
+		rm -f "$TMP_HEAD"
+		test "$rsync_slurped_objects" || {
+			rsync -avz --ignore-existing "$remote/objects/" \
+				"$GIT_OBJECT_DIRECTORY/" || exit
+			rsync_slurped_objects=t
+		}
+		;;
+	*)
+		# We will do git native transport at one go.
+		continue ;;
+	esac
+	head=$(git-rev-parse --verify "$head^0") || exit
+	echo "$head	$remote_name from $remote_nick" >>$GIT_DIR/FETCH_HEAD
+	case "$local_name" in '') continue ;; esac
+
+	# We are storing the head locally.  Make sure that it is
+	# a fast forward (aka "reverse push").
+	fast_forward_local "$local_name" "$head" "$remote" "$remote_name"
+
+done
+
+case "$remote" in
+http://* | https://* | rsync://* )
+	;; # we are already done.
+*)
+	git-fetch-pack "$remote" $rref |
+	while read sha1 remote_name
+	do
+		found=
+		for ref in $refs
+		do
+			case "$ref" in
+			$remote_name:*)
+				found="$ref"
+				break ;;
+			esac
+		done
+		head=$(git-rev-parse --verify "$sha1^0") || exit
+		echo "$head	$remote_name from $remote_nick" >>$GIT_DIR/FETCH_HEAD
+		case "$found" in '' | *:) continue ;; esac
+		local=$(expr "$found" : '[^:]*:\(.*\)')
+		fast_forward_local "$local" "$sha1" "$remote" "$remote_name"
+	done
+	;;
+esac

[PATCH 1/3] Start adding the $GIT_DIR/remotes/ support.

From: Junio C Hamano <hidden>
Date: 2016-06-15 22:42:04

All the necessary parsing code is in git-parse-remote-script;
update git-push-script to use it.

Signed-off-by: Junio C Hamano <redacted>
---

 Makefile                |    2 -
 git-parse-remote-script |  122 +++++++++++++++++++++++++++++++++++++++++++++++
 git-push-script         |   28 ++---------
 3 files changed, 129 insertions(+), 23 deletions(-)
 create mode 100755 git-parse-remote-script

f8892bf17675056cd18a252d3bc4e4ba381fb3bc
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -64,7 +64,7 @@ SCRIPTS=git git-apply-patch-script git-m
 	git-reset-script git-add-script git-checkout-script git-clone-script \
 	gitk git-cherry git-rebase-script git-relink-script git-repack-script \
 	git-format-patch-script git-sh-setup-script git-push-script \
-	git-branch-script git-parse-remote git-verify-tag-script \
+	git-branch-script git-parse-remote git-parse-remote-script git-verify-tag-script \
 	git-ls-remote-script git-clone-dumb-http git-rename-script \
 	git-request-pull-script git-bisect-script
 
diff --git a/git-parse-remote-script b/git-parse-remote-script
new file mode 100755
--- /dev/null
+++ b/git-parse-remote-script
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+. git-sh-setup-script || die "Not a git archive"
+
+get_data_source () {
+	case "$1" in
+	*/*)
+		# Not so fast.  This could be the partial URL shorthand...
+		token=$(expr "$1" : '\([^/]*\)/')
+		remainder=$(expr "$1" : '[^/]*/\(.*\)')
+		if test -f "$GIT_DIR/branches/$token"
+		then
+			echo branches-partial
+		else
+			echo ''
+		fi
+		;;
+	*)
+		if test -f "$GIT_DIR/remotes/$1"
+		then
+			echo remotes
+		elif test -f "$GIT_DIR/branches/$1"
+		then
+			echo branches
+		else
+			echo ''
+		fi ;;
+	esac
+}
+
+get_remote_url () {
+	data_source=$(get_data_source "$1")
+	case "$data_source" in
+	'')
+		echo "$1" ;;
+	remotes)
+		sed -ne '/^URL: */{
+			s///p
+			q
+		}' "$GIT_DIR/remotes/$1" ;;
+	branches)
+		sed -e 's/#.*//' "$GIT_DIR/branches/$1" ;;
+	branches-partial)
+		token=$(expr "$1" : '\([^/]*\)/')
+		remainder=$(expr "$1" : '[^/]*/\(.*\)')
+		url=$(sed -e 's/#.*//' "$GIT_DIR/branches/$token")
+		echo "$url/$remainder"
+		;;
+	*)
+		die "internal error: get-remote-url $1" ;;
+	esac
+}
+
+get_remote_default_refs_for_push () {
+	data_source=$(get_data_source "$1")
+	case "$data_source" in
+	'' | branches | branches-partial)
+		;; # no default push mapping, just send matching refs.
+	remotes)
+		sed -ne '/^Push: */{
+			s///p
+		}' "$GIT_DIR/remotes/$1" ;;
+	*)
+		die "internal error: get-remote-default-ref-for-push $1" ;;
+	esac
+}
+
+# Subroutine to caninicalize remote:local notation
+canon_refs_list_for_fetch () {
+	for ref
+	do
+		expr "$ref" : '.*:' >/dev/null || ref="${ref}:"
+		remote=$(expr "$ref" : '\([^:]*\):')
+		local=$(expr "$ref" : '[^:]*:\(.*\)')
+		case "$remote" in
+		'') remote=HEAD ;;
+		*) remote="refs/heads/$remote" ;;
+		esac
+		case "$local" in
+		'') local= ;;
+		*) local="refs/heads/$local" ;;
+		esac
+		echo "${remote}:${local}"
+	done
+}
+
+# Returns list of src: (no store), or src:dst (store)
+get_remote_default_refs_for_fetch () {
+	data_source=$(get_data_source "$1")
+	case "$data_source" in
+	'' | branches-partial)
+		echo "HEAD:" ;;
+	branches)
+		remote_branch=$(sed -ne '/#/s/.*#//p' "$GIT_DIR/branches/$1")
+		case "$remote_branch" in '') remote_branch=master ;; esac
+		echo "refs/heads/${remote_branch}:refs/heads/$1"
+		;;
+	remotes)
+		canon_refs_list_for_fetch $(sed -ne '/^Pull: */{
+						s///p
+					}' "$GIT_DIR/remotes/$1")
+		;;
+	*)
+		die "internal error: get-remote-default-ref-for-push $1" ;;
+	esac
+}
+
+get_remote_refs_for_push () {
+	case "$#" in
+	0) die "internal error: get-remote-refs-for-push." ;;
+	1) get_remote_default_refs_for_push "$@" ;;
+	*) shift; echo "$@" ;;
+	esac
+}
+
+get_remote_refs_for_fetch () {
+	case "$#" in
+	0) die "internal error: get-remote-refs-for-fetch." ;;
+	1) get_remote_default_refs_for_fetch "$@" ;;
+	*) shift; canon_refs_list_for_fetch "$@" ;;
+	esac
+}
diff --git a/git-push-script b/git-push-script
--- a/git-push-script
+++ b/git-push-script
@@ -20,8 +20,6 @@ do
 	-*)
 		die "Unknown parameter $1" ;;
         *)
-		remote="$1"
-		shift
 		set x "$@"
 		shift
 		break ;;
@@ -29,27 +27,13 @@ do
 	shift
 done
 
-case "$remote" in
-*:* | /* | ../* | ./* )
-	# An URL, host:/path/to/git, absolute and relative paths.
-	;;
-* )
-	# Shorthand
-	if expr "$remote" : '..*/..*' >/dev/null
-	then
-		# a short-hand followed by a trailing path
-		shorthand=$(expr "$remote" : '\([^/]*\)')
-		remainder=$(expr "$remote" : '[^/]*\(/.*\)$')
-	else
-		shorthand="$remote"
-		remainder=
-	fi
-	remote=$(sed -e 's/#.*//' "$GIT_DIR/branches/$remote") &&
-	expr "$remote" : '..*:' >/dev/null &&
-	remote="$remote$remainder" ||
-	die "Cannot parse remote $remote"
-	;;
+. git-parse-remote-script
+remote=$(get_remote_url "$@")
+case "$has_all" in
+--all) set x ;;
+'')    set x $(get_remote_refs_for_push "$@") ;;
 esac
+shift
 
 case "$remote" in
 http://* | https://* | git://* | rsync://* )

[PATCH 3/3] Update git-pull to match updated git-fetch.

From: Junio C Hamano <hidden>
Date: 2016-06-15 22:42:04

This retires the git-parse-remote script, and allows pull to fetch
from multiple remote references.  There is no support for resolving
more than two heads, so that would be next.

Signed-off-by: Junio C Hamano <redacted>
---

 Makefile         |    2 +
 git-parse-remote |   79 ------------------------------------------------------
 git-pull-script  |   14 ++++++----
 3 files changed, 10 insertions(+), 85 deletions(-)
 delete mode 100755 git-parse-remote

3a071a02828c71bbfdc2749d25814906cd9c8b18
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -64,7 +64,7 @@ SCRIPTS=git git-apply-patch-script git-m
 	git-reset-script git-add-script git-checkout-script git-clone-script \
 	gitk git-cherry git-rebase-script git-relink-script git-repack-script \
 	git-format-patch-script git-sh-setup-script git-push-script \
-	git-branch-script git-parse-remote git-parse-remote-script git-verify-tag-script \
+	git-branch-script git-parse-remote-script git-verify-tag-script \
 	git-ls-remote-script git-clone-dumb-http git-rename-script \
 	git-request-pull-script git-bisect-script
 
diff --git a/git-parse-remote b/git-parse-remote
deleted file mode 100755
--- a/git-parse-remote
+++ /dev/null
@@ -1,79 +0,0 @@
-: To be included in git-pull and git-fetch scripts.
-
-# A remote repository can be specified on the command line
-# in one of the following formats:
-#
-#	<repo>
-#	<repo> <head>
-#	<repo> tag <tag>
-#
-# where <repo> could be one of:
-#
-#	a URL (including absolute or local pathname)
-#	a short-hand
-#	a short-hand followed by a trailing path
-#
-# A short-hand <name> has a corresponding file $GIT_DIR/branches/<name>,
-# whose contents is a URL, possibly followed by a URL fragment #<head>
-# to name the default branch on the remote side to fetch from.
-
-_remote_repo= _remote_store= _remote_head= _remote_name=
-
-case "$1" in
-*:* | /* | ../* | ./* )
-	_remote_repo="$1"
-	;;
-* )
-	# otherwise, it is a short hand.
-	case "$1" in
-	*/*)
-		# a short-hand followed by a trailing path
-		_token=$(expr "$1" : '\([^/]*\)/')
-		_rest=$(expr "$1" : '[^/]*\(/.*\)$')
-		;;
-	*)
-		_token="$1"
-		_rest=
-		_remote_store="refs/heads/$_token"
-		;;
-	esac
-	test -f "$GIT_DIR/branches/$_token" ||
-	die "No such remote branch: $_token"
-
-	_remote_repo=$(cat "$GIT_DIR/branches/$_token")"$_rest"
-	;;
-esac
-
-case "$_remote_repo" in
-*"#"*)
-	_remote_head=`expr "$_remote_repo" : '.*#\(.*\)$'`
-	_remote_repo=`expr "$_remote_repo" : '\(.*\)#'`
-	;;
-esac
-
-_remote_name=$(echo "$_remote_repo" | sed 's|\.git/*$||')
-
-case "$2" in
-tag)
-	_remote_name="tag '$3' of $_remote_name"
-	_remote_head="refs/tags/$3"
-	_remote_store="$_remote_head"
-	;;
-?*)
-	# command line specified a head explicitly; do not
-	# store the fetched head as a branch head.
-	_remote_name="head '$2' of $_remote_name"
-	_remote_head="refs/heads/$2"
-	_remote_store=''
-	;;
-'')
-	case "$_remote_head" in
-	'')
-		_remote_head=HEAD ;;
-	*)
-		_remote_name="head '$_remote_head' of $_remote_name"
-		_remote_head="refs/heads/$_remote_head"
-		;;
-	esac
-	;;
-esac
diff --git a/git-pull-script b/git-pull-script
--- a/git-pull-script
+++ b/git-pull-script
@@ -1,12 +1,16 @@
 #!/bin/sh
 #
 . git-sh-setup-script || die "Not a git archive"
-. git-parse-remote "$@"
-merge_name="$_remote_name"
-
 git-fetch-script "$@" || exit 1
+merge_head=$(sed -e 's/	.*//' "$GIT_DIR"/FETCH_HEAD | tr '\012' ' ')
+merge_name=$(sed -e 's/^[0-9a-f]*	//' "$GIT_DIR"/FETCH_HEAD |
+	 tr '\012' ' ')
+
+case "$merge_head" in
+'' | *' '?*) die "Cannot resolve multiple heads at the same time (yet)." ;;
+esac
+
 
 git-resolve-script \
 	"$(cat "$GIT_DIR"/HEAD)" \
-	"$(cat "$GIT_DIR"/FETCH_HEAD)" \
-	"Merge $merge_name"
+	$merge_head "Merge $merge_name"

Re: Multi-head pulling series

From: Josef Weidendorfer <hidden>
Date: 2016-06-15 22:42:04

On Thursday 18 August 2005 09:24, Junio C Hamano wrote:
	$ cat $GIT_DIR/remotes/www
	URL: http://www.kernel.org/pub/scm/git/git.git/
	Pull: master:ko-master pu:ko-pu
      Push: master:master pu:pu foo:bar
Isn't this mixing two kinds of information:
1) Some default/persistent mapping of local to remote heads
2) The default heads which are pulled/pushed when only giving the remote, like 
in:
	$ git push mko ;# push our master and pu to the same
			# name, foo to .git/refs/heads/bar.
I think it makes sense to be able to store mappings without adding the head to
the default group of heads pulled.

Can we put the default pull/push actions in separate lines, like

 	$ cat $GIT_DIR/remotes/mko
 	URL: master.kernel.org:/pub/scm/git/git.git/
 	Pull: master:ko-master pu:ko-pu mylocal:myremote
	Push: master:master pu:pu foo:bar
	Default-Pull: master pu
	Default-Push: master pu foo
	
	$ git push mko pu:refs/heads/testing
			# instead of pushing to the usual ref,
                        # push our pu to refs/heads/testing,
                        # this time only.
With a command (push/fetch/pull) giving an explicit local/remote mapping, it 
would be cool to automatically add the given mapping to the remotes/ file if 
there is no push-mapping for pu yet, so that you can have the same later with 
only

	git push mko pu

And finally, it would be nice to specify a default mapping for arbitrary heads

	$ cat $GIT_DIR/remotes/www
 	URL: http://www.kernel.org/pub/scm/git/git.git/
 	Pull: www#*:*

Such that

	git fetch www#pu

will fetch head pu into local head .refs/heads/www#pu


Josef

PS: I know I should provide patches for my proposals. But let discuss them 
first.
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help