Skip to contentSkip to conversation

ref-filter: add %(notes) format atom

PATCH In review ZheNing Hu via GitGitGadget 4 participants 5 messages Apr 29, 2021 – May 3, 2021 Documentation

lore
Share & tools
lore
#0 lore raw

PATCH 1/1 ref-filter: add %(notes) format atom

From: ZheNing Hu <[email protected]>

Note that --pretty="%N" can view notes related to the commit. So add %(notes) to ref-filter seem is a good choice. This atom %(notes) view the notes associated with the ref, and support dereference.

---
[GSOC] ref-filter: add %(notes) format atom

An important step in the GSOC project Use ref-filter formats in git cat-file is to integrate different format atoms into the ref-filter. Olga and Hariom have also made a lot of efforts in this area.

Currently, I noticed that there may be some format atoms in "pretty.c" that have not been migrated to ref-filter, such as --pretty="%N", --pretty="%(describe)".

So in this patch, I tried to migrate --pretty=%N to --format=%(notes).

Hope this will be hopeful !!!

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-944%2Fadlternative%2Fformat-notes-atom-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-944/adlternative/format-notes-atom-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/944

 Documentation/git-for-each-ref.txt |  4 ++++
 ref-filter.c                       | 31 ++++++++++++++++++++++++++++--
 t/t6300-for-each-ref.sh            | 10 ++++++++++
 3 files changed, 43 insertions(+), 2 deletions(-)
Documentation/git-for-each-ref.txt +4 -0
index 2ae2478de706..07f037a16e13 100644
@@ -139,6 +139,9 @@ deltabase::
 	given object, if it is stored as a delta.  Otherwise it
 	expands to the null object name (all zeroes).
 
+notes::
+	The notes associated with the ref.
+
 upstream::
 	The name of a local ref which can be considered ``upstream''
 	from the displayed ref. Respects `:short`, `:lstrip` and
@@ -302,6 +305,7 @@ git for-each-ref --count=3 --sort='-*authordate' \
 Subject: %(*subject)
 Date: %(*authordate)
 Ref: %(*refname)
+Notes: %(*notes)
 
 %(*body)
 ' 'refs/tags'
ref-filter.c +29 -2
index a0adb4551d87..42a5608a3056 100644
@@ -23,6 +23,7 @@
 #include "worktree.h"
 #include "hashmap.h"
 #include "strvec.h"
+#include "run-command.h"
 
 static struct ref_msg {
 	const char *gone;
@@ -506,6 +507,7 @@ static struct {
 	{ "objectsize", SOURCE_OTHER, FIELD_ULONG, objectsize_atom_parser },
 	{ "objectname", SOURCE_OTHER, FIELD_STR, oid_atom_parser },
 	{ "deltabase", SOURCE_OTHER, FIELD_STR, deltabase_atom_parser },
+	{ "notes", SOURCE_OTHER, FIELD_STR },
 	{ "tree", SOURCE_OBJ, FIELD_STR, oid_atom_parser },
 	{ "parent", SOURCE_OBJ, FIELD_STR, oid_atom_parser },
 	{ "numparent", SOURCE_OBJ, FIELD_ULONG },
@@ -953,6 +955,24 @@ static int grab_oid(const char *name, const char *field, const struct object_id
 	return 0;
 }
 
+static int grab_notes(const struct object_id *oid, struct atom_value *v)
+{
+	struct child_process cmd = CHILD_PROCESS_INIT;
+	struct strbuf out = STRBUF_INIT;
+	struct strbuf err = STRBUF_INIT;
+	const char *args[] = { "notes", "show", NULL };
+	int ret;
+
+	cmd.git_cmd = 1;
+	strvec_pushv(&cmd.args, args);
+	strvec_push(&cmd.args, oid_to_hex(oid));
+	ret = pipe_command(&cmd, NULL, 0, &out, 0, &err, 0);
+	strbuf_trim_trailing_newline(&out);
+	v->s = strbuf_detach(&out, NULL);
+	strbuf_release(&err);
+	return ret;
+}
+
 /* See grab_values */
 static void grab_common_values(struct atom_value *val, int deref, struct expand_data *oi)
 {
@@ -975,8 +995,12 @@ static void grab_common_values(struct atom_value *val, int deref, struct expand_
 			v->s = xstrfmt("%"PRIuMAX , (uintmax_t)oi->size);
 		} else if (!strcmp(name, "deltabase"))
 			v->s = xstrdup(oid_to_hex(&oi->delta_base_oid));
-		else if (deref)
-			grab_oid(name, "objectname", &oi->oid, v, &used_atom[i]);
+		else if (deref) {
+			if (!strcmp(name, "notes"))
+				grab_notes(&oi->oid, v);
+			else
+				grab_oid(name, "objectname", &oi->oid, v, &used_atom[i]);
+		}
 	}
 }
 
@@ -1767,6 +1791,9 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
 			continue;
 		} else if (!deref && grab_oid(name, "objectname", &ref->objectname, v, atom)) {
 			continue;
+		} else if (!deref && !strcmp(name, "notes")) {
+			grab_notes(&ref->objectname, v);
+			continue;
 		} else if (!strcmp(name, "HEAD")) {
 			if (atom->u.head && !strcmp(ref->refname, atom->u.head))
 				v->s = xstrdup("*");
t/t6300-for-each-ref.sh +10 -0
index 9e0214076b4d..61cdbeb696ff 100755
@@ -32,8 +32,10 @@ test_expect_success setup '
 	git add one &&
 	git commit -m "Initial" &&
 	git branch -M main &&
+	git notes add -m "commit-notes" HEAD &&
 	setdate_and_increment &&
 	git tag -a -m "Tagging at $datestamp" testtag &&
+	git notes add -m "tag-notes" testtag &&
 	git update-ref refs/remotes/origin/main main &&
 	git remote add origin nowhere &&
 	git config branch.main.remote origin &&
@@ -162,6 +164,7 @@ test_atom head contents:signature ''
 test_atom head contents 'Initial
 '
 test_atom head HEAD '*'
+test_atom head notes $(git notes show refs/heads/main)
 
 test_atom tag refname refs/tags/testtag
 test_atom tag refname:short testtag
@@ -220,6 +223,8 @@ test_atom tag contents:signature ''
 test_atom tag contents 'Tagging at 1151968727
 '
 test_atom tag HEAD ' '
+test_atom tag notes $(git notes show refs/tags/testtag)
+test_atom tag "*notes" $(git notes show refs/heads/main)
 
 test_expect_success 'Check invalid atoms names are errors' '
 	test_must_fail git for-each-ref --format="%(INVALID)" refs/heads
@@ -380,6 +385,7 @@ test_expect_success 'exercise strftime with odd fields' '
 
 cat >expected <<\EOF
 refs/heads/main
+refs/notes/commits
 refs/remotes/origin/main
 refs/tags/testtag
 EOF
@@ -393,6 +399,7 @@ test_expect_success 'Verify ascending sort' '
 cat >expected <<\EOF
 refs/tags/testtag
 refs/remotes/origin/main
+refs/notes/commits
 refs/heads/main
 EOF
 
@@ -429,6 +436,7 @@ test_expect_success 'exercise glob patterns with prefixes' '
 
 cat >expected <<\EOF
 'refs/heads/main'
+'refs/notes/commits'
 'refs/remotes/origin/main'
 'refs/tags/testtag'
 EOF
@@ -450,6 +458,7 @@ test_expect_success 'Quoting style: python' '
 
 cat >expected <<\EOF
 "refs/heads/main"
+"refs/notes/commits"
 "refs/remotes/origin/main"
 "refs/tags/testtag"
 EOF
@@ -509,6 +518,7 @@ test_expect_success 'Check for invalid refname format' '
 test_expect_success 'set up color tests' '
 	cat >expected.color <<-EOF &&
 	$(git rev-parse --short refs/heads/main) <GREEN>main<RESET>
+	$(git rev-parse --short refs/notes/commits) <GREEN>notes/commits<RESET>
 	$(git rev-parse --short refs/remotes/myfork/main) <GREEN>myfork/main<RESET>
 	$(git rev-parse --short refs/remotes/origin/main) <GREEN>origin/main<RESET>
 	$(git rev-parse --short refs/tags/testtag) <GREEN>testtag<RESET>

base-commit: 311531c9de557d25ac087c1637818bd2aad6eb3a

Signature · 1 line
gitgitgadget
#1 lore raw

Am 29.04.21 um 12:24 schrieb ZheNing Hu via GitGitGadget:

Quoted 7 lines

From: ZheNing Hu <[email protected]>

Note that --pretty="%N" can view notes related to the commit. So add %(notes) to ref-filter seem is a good choice. This atom %(notes) view the notes associated with the ref, and support dereference.

I can't judge the usefulness of this feature because I don't use notes.

Quoted 7 lines

---
[GSOC] ref-filter: add %(notes) format atom

An important step in the GSOC project Use ref-filter formats in git cat-file is to integrate different format atoms into the ref-filter. Olga and Hariom have also made a lot of efforts in this area.

This is done to replace the custom format parser in git cat-file with ref-filter in order to reduce code duplication, right?

Quoted 3 lines

Currently, I noticed that there may be some format atoms in "pretty.c" that have not been migrated to ref-filter, such as --pretty="%N", --pretty="%(describe)".

git cat-file doesn't support pretty format atoms, so I'm not sure I see the connection here.

Quoted 61 lines

So in this patch, I tried to migrate --pretty=%N to --format=%(notes).

Hope this will be hopeful !!!

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-944%2Fadlternative%2Fformat-notes-atom-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-944/adlternative/format-notes-atom-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/944

 Documentation/git-for-each-ref.txt |  4 ++++
 ref-filter.c                       | 31 ++++++++++++++++++++++++++++--
 t/t6300-for-each-ref.sh            | 10 ++++++++++
 3 files changed, 43 insertions(+), 2 deletions(-)
Documentation/git-for-each-ref.txt +4 -0
index 2ae2478de706..07f037a16e13 100644
@@ -139,6 +139,9 @@ deltabase::
 	given object, if it is stored as a delta.  Otherwise it
 	expands to the null object name (all zeroes).
 
+notes::
+	The notes associated with the ref.
+
 upstream::
 	The name of a local ref which can be considered ``upstream''
 	from the displayed ref. Respects `:short`, `:lstrip` and
@@ -302,6 +305,7 @@ git for-each-ref --count=3 --sort='-*authordate' \
 Subject: %(*subject)
 Date: %(*authordate)
 Ref: %(*refname)
+Notes: %(*notes)
 
 %(*body)
 ' 'refs/tags'
ref-filter.c +4 -0
index a0adb4551d87..42a5608a3056 100644
@@ -23,6 +23,7 @@
 #include "worktree.h"
 #include "hashmap.h"
 #include "strvec.h"
+#include "run-command.h"
 
 static struct ref_msg {
 	const char *gone;
@@ -506,6 +507,7 @@ static struct {
 	{ "objectsize", SOURCE_OTHER, FIELD_ULONG, objectsize_atom_parser },
 	{ "objectname", SOURCE_OTHER, FIELD_STR, oid_atom_parser },
 	{ "deltabase", SOURCE_OTHER, FIELD_STR, deltabase_atom_parser },
+	{ "notes", SOURCE_OTHER, FIELD_STR },
 	{ "tree", SOURCE_OBJ, FIELD_STR, oid_atom_parser },
 	{ "parent", SOURCE_OBJ, FIELD_STR, oid_atom_parser },
 	{ "numparent", SOURCE_OBJ, FIELD_ULONG },
@@ -953,6 +955,24 @@ static int grab_oid(const char *name, const char *field, const struct object_id
 	return 0;
 }
 
+static int grab_notes(const struct object_id *oid, struct atom_value *v)
+{

ref-filter.c static int grab_oid(const char *name, const char *field, const struct object_id · line 969

Quoted 10 lines
ref-filter.c +10 -0
+	struct child_process cmd = CHILD_PROCESS_INIT;
+	struct strbuf out = STRBUF_INIT;
+	struct strbuf err = STRBUF_INIT;
+	const char *args[] = { "notes", "show", NULL };
+	int ret;
+
+	cmd.git_cmd = 1;
+	strvec_pushv(&cmd.args, args);
+	strvec_push(&cmd.args, oid_to_hex(oid));
+	ret = pipe_command(&cmd, NULL, 0, &out, 0, &err, 0);

Nice prototype. Is it possible to avoid the overhead of calling git notes as an external process by calling load_display_notes() once at parse time and format_display_notes() for each item? Would it cause conflicts for code that uses ref-filter and handles notes already?

Quoted 34 lines
ref-filter.c +15 -2
+	strbuf_trim_trailing_newline(&out);
+	v->s = strbuf_detach(&out, NULL);
+	strbuf_release(&err);
+	return ret;
+}
+
 /* See grab_values */
 static void grab_common_values(struct atom_value *val, int deref, struct expand_data *oi)
 {
@@ -975,8 +995,12 @@ static void grab_common_values(struct atom_value *val, int deref, struct expand_
 			v->s = xstrfmt("%"PRIuMAX , (uintmax_t)oi->size);
 		} else if (!strcmp(name, "deltabase"))
 			v->s = xstrdup(oid_to_hex(&oi->delta_base_oid));
-		else if (deref)
-			grab_oid(name, "objectname", &oi->oid, v, &used_atom[i]);
+		else if (deref) {
+			if (!strcmp(name, "notes"))
+				grab_notes(&oi->oid, v);
+			else
+				grab_oid(name, "objectname", &oi->oid, v, &used_atom[i]);
+		}
 	}
 }
 
@@ -1767,6 +1791,9 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
 			continue;
 		} else if (!deref && grab_oid(name, "objectname", &ref->objectname, v, atom)) {
 			continue;
+		} else if (!deref && !strcmp(name, "notes")) {
+			grab_notes(&ref->objectname, v);
+			continue;
 		} else if (!strcmp(name, "HEAD")) {
 			if (atom->u.head && !strcmp(ref->refname, atom->u.head))
 				v->s = xstrdup("*");
#2 lore raw
Quoted 23 lines · René Scharfe <[email protected]> 于2021年5月2日周日 上午3:21写道:

Am 29.04.21 um 12:24 schrieb ZheNing Hu via GitGitGadget:

Quoted 7 lines

From: ZheNing Hu <[email protected]>

Note that --pretty="%N" can view notes related to the commit. So add %(notes) to ref-filter seem is a good choice. This atom %(notes) view the notes associated with the ref, and support dereference.

I can't judge the usefulness of this feature because I don't use notes.

Quoted 7 lines

---
[GSOC] ref-filter: add %(notes) format atom

An important step in the GSOC project Use ref-filter formats in git cat-file is to integrate different format atoms into the ref-filter. Olga and Hariom have also made a lot of efforts in this area.

This is done to replace the custom format parser in git cat-file with ref-filter in order to reduce code duplication, right?

Quoted 7 lines
Quoted 3 lines

Currently, I noticed that there may be some format atoms in "pretty.c" that have not been migrated to ref-filter, such as --pretty="%N", --pretty="%(describe)".

git cat-file doesn't support pretty format atoms, so I'm not sure I see the connection here.

At present, git cat-file --batch, git log --pretty, and git for-each-ref --format
lack consistency.

The ambition of this project is to unify these different formats "%aN","%(authorname)"
through an interface, maybe it's ref-filter or maybe a new one.

It is estimated that many places in need to be replaced and rebuilt, and it not just to
reduce code duplication.

Note that cat-file --batch should be a superset of ref-filter and --pretty, because
it supports all objects, while ref-filter only supports "commit" and "tag" two kinds
of objects, and --pretty only supports "commit" one kind of object.

So it may be reasonable to provide %(notes) to "commit","tag" objects in cat-file --batch
in the future, now integrate them into ref-filter firstly.

Quoted 68 lines
Quoted 68 lines

So in this patch, I tried to migrate --pretty=%N to --format=%(notes).

Hope this will be hopeful !!!

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-944%2Fadlternative%2Fformat-notes-atom-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-944/adlternative/format-notes-atom-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/944

 Documentation/git-for-each-ref.txt |  4 ++++
 ref-filter.c                       | 31 ++++++++++++++++++++++++++++--
 t/t6300-for-each-ref.sh            | 10 ++++++++++
 3 files changed, 43 insertions(+), 2 deletions(-)
Documentation/git-for-each-ref.txt +4 -0
index 2ae2478de706..07f037a16e13 100644
@@ -139,6 +139,9 @@ deltabase::
      given object, if it is stored as a delta.  Otherwise it
      expands to the null object name (all zeroes).
 
+notes::
+     The notes associated with the ref.
+
 upstream::
      The name of a local ref which can be considered ``upstream''
      from the displayed ref. Respects `:short`, `:lstrip` and
@@ -302,6 +305,7 @@ git for-each-ref --count=3 --sort='-*authordate' \
 Subject: %(*subject)
 Date: %(*authordate)
 Ref: %(*refname)
+Notes: %(*notes)
 
 %(*body)
 ' 'refs/tags'
ref-filter.c +11 -0
index a0adb4551d87..42a5608a3056 100644
@@ -23,6 +23,7 @@
 #include "worktree.h"
 #include "hashmap.h"
 #include "strvec.h"
+#include "run-command.h"
 
 static struct ref_msg {
      const char *gone;
@@ -506,6 +507,7 @@ static struct {
      { "objectsize", SOURCE_OTHER, FIELD_ULONG, objectsize_atom_parser },
      { "objectname", SOURCE_OTHER, FIELD_STR, oid_atom_parser },
      { "deltabase", SOURCE_OTHER, FIELD_STR, deltabase_atom_parser },
+     { "notes", SOURCE_OTHER, FIELD_STR },
      { "tree", SOURCE_OBJ, FIELD_STR, oid_atom_parser },
      { "parent", SOURCE_OBJ, FIELD_STR, oid_atom_parser },
      { "numparent", SOURCE_OBJ, FIELD_ULONG },
@@ -953,6 +955,24 @@ static int grab_oid(const char *name, const char *field, const struct object_id
      return 0;
 }
 
+static int grab_notes(const struct object_id *oid, struct atom_value *v)
+{
+     struct child_process cmd = CHILD_PROCESS_INIT;
+     struct strbuf out = STRBUF_INIT;
+     struct strbuf err = STRBUF_INIT;
+     const char *args[] = { "notes", "show", NULL };
+     int ret;
+
+     cmd.git_cmd = 1;
Quoted 9 lines

ref-filter.c static int grab_oid(const char *name, const char *field, const struct object_id · line 969

Quoted 3 lines
ref-filter.c +3 -0
+     strvec_pushv(&cmd.args, args);
+     strvec_push(&cmd.args, oid_to_hex(oid));
+     ret = pipe_command(&cmd, NULL, 0, &out, 0, &err, 0);

Nice prototype. Is it possible to avoid the overhead of calling git notes as an external process by calling load_display_notes() once at parse time and format_display_notes() for each item? Would it cause conflicts for code that uses ref-filter and handles notes already?

This sounds feasible, I will try if it can work.

Thanks!
--
ZheNing Hu

#3 lore raw
Quoted 5 lines · ZheNing Hu

Note that cat-file --batch should be a superset of ref-filter and --pretty, because
it supports all objects, while ref-filter only supports "commit" and "tag" two kinds
of objects, and --pretty only supports "commit" one kind of object.

What? A ref can point at any kind of objects, not necessarily commits and tags. %(objectname), %(objecttype), etc. obviously are applicable to any type of object.

Another thing worth noting is that ref-filter needs to deal with traits that are not tied to any particular object, but to the ref itself, like who its upstream is and where it would be pushed to.

Quoted 3 lines

So it may be reasonable to provide %(notes) to "commit","tag" objects in cat-file --batch
in the future, now integrate them into ref-filter firstly.

And you can attach notes to objects of any type, not limited to commits and tags.

Quoted 1 line
Quoted 1 line
Quoted 1 line

So in this patch, I tried to migrate --pretty=%N to --format=%(notes).

What do you mean by "migrate"? Are you making both available?

Quoted 10 lines
Quoted 10 lines

Documentation/git-for-each-ref.txt deltabase:: · line 143

Quoted 10 lines
Documentation/git-for-each-ref.txt +2 -0
index 2ae2478de706..07f037a16e13 100644
@@ -139,6 +139,9 @@ deltabase::
      given object, if it is stored as a delta.  Otherwise it
      expands to the null object name (all zeroes).
 
+notes::
+     The notes associated with the ref.

"The notes associated with the object pointed at by the ref".

A note is never associated with a ref. It can only associated with an object.

If we are going to have %(notes), it may be natural to desire grabbing notes for the given object from specified notes ref, e.g.

    git for-each-ref --format="%(notes:amlog) %(subject)" zh/pretty-date-human

may want to give the same kind of information as

    git show -s --format='%N %s' --notes=amlog zh/pretty-date-human

The underlying notes machinery however may not be prepared to work with more than one notes tree at the same time, so

    git for-each-ref --format="%(notes) %(subject)" --notes=amlog zh/pretty-date-human

might be a better syntax, as --format="%(notes:X) %(notes:Y)" may not be something you can easily support.

#4 lore raw
Quoted 9 lines · Junio C Hamano <[email protected]> 于2021年5月3日周一 上午10:57写道:
Quoted 5 lines · ZheNing Hu

Note that cat-file --batch should be a superset of ref-filter and --pretty, because
it supports all objects, while ref-filter only supports "commit" and "tag" two kinds
of objects, and --pretty only supports "commit" one kind of object.

Quoted 4 lines

What? A ref can point at any kind of objects, not necessarily commits and tags. %(objectname), %(objecttype), etc. obviously are applicable to any type of object.

Thanks for correcting. I only saw tag and commit in the output of git for-each-ref before, and blob and tree are not processed in grab_values in ref-filter.c, which deepens my illusion. git update-ref can make a ref point to a blob object or tree object, although I don't know what these refs are used for... A reference to a commit object makes sense, but if it is a blob, are there any suitable scenarios?

Quoted 4 lines

Another thing worth noting is that ref-filter needs to deal with traits that are not tied to any particular object, but to the ref itself, like who its upstream is and where it would be pushed to.

Indeed need to be considered "%(push)" "%(remote)" will change "refname".

Quoted 11 lines
Quoted 3 lines

So it may be reasonable to provide %(notes) to "commit","tag" objects in cat-file --batch
in the future, now integrate them into ref-filter firstly.

And you can attach notes to objects of any type, not limited to commits and tags.

Quoted 1 line
Quoted 1 line
Quoted 1 line

So in this patch, I tried to migrate --pretty=%N to --format=%(notes).

What do you mean by "migrate"? Are you making both available?

Well, here are some incorrect statements, I just want to express that the --format="%(notes)" implemented in "ref-filter" has the same semantics as "--pretty=%N".

Quoted 8 lines
Quoted 8 lines
Quoted 8 lines
Quoted 8 lines
Documentation/git-for-each-ref.txt +0 -0
index 2ae2478de706..07f037a16e13 100644
@@ -139,6 +139,9 @@ deltabase::
      given object, if it is stored as a delta.  Otherwise it
      expands to the null object name (all zeroes).
 
Quoted 8 lines
Quoted 2 lines
Quoted 2 lines

Documentation/git-for-each-ref.txt deltabase:: · line 143

Quoted 2 lines
Documentation/git-for-each-ref.txt +2 -0
+notes::
+     The notes associated with the ref.

"The notes associated with the object pointed at by the ref".

A note is never associated with a ref. It can only associated with an object.

Indeed.

Quoted 9 lines

If we are going to have %(notes), it may be natural to desire grabbing notes for the given object from specified notes ref, e.g.

    git for-each-ref --format="%(notes:amlog) %(subject)" zh/pretty-date-human

may want to give the same kind of information as

    git show -s --format='%N %s' --notes=amlog zh/pretty-date-human
Quoted 8 lines

The underlying notes machinery however may not be prepared to work with more than one notes tree at the same time, so

    git for-each-ref --format="%(notes) %(subject)" --notes=amlog zh/pretty-date-human

might be a better syntax, as --format="%(notes:X) %(notes:Y)" may not be something you can easily support.

Here is a "bad" idea that might support this feature: use another subprocess "git notes --refs=refs/notes/amlog" :)

Thanks.
--
ZheNing Hu