From: Alexander Turenko <alexander.turenko@tarantool.org> To: Vladimir Davydov <vdavydov.dev@gmail.com> Cc: tarantool-patches@freelists.org Subject: Re: [PATCH v2 2/6] Add functions to ease using Lua iterators from C Date: Wed, 16 Jan 2019 02:26:23 +0300 [thread overview] Message-ID: <20190115232623.huxpqj4adoqqi3mo@tkn_work_nb> (raw) In-Reply-To: <20190110122909.novsa6o6duykot6a@esperanza> Thanks for the review! I commented inline and fixed all comments except one, where I doubt. The patch from the previous version at end of the email. List of changes: * Fixed zero gen() retvals case. * Added a unit test. * Removed unneded cast from (void *). * Allow to pass values w/o a table. - Also removed _fromtable suffix from luaL_iterator_new(). * luaL_iterator_free() -> luaL_iterator_delete(). WBR, Alexander Turenko. On Thu, Jan 10, 2019 at 03:29:09PM +0300, Vladimir Davydov wrote: > On Wed, Jan 09, 2019 at 11:20:10PM +0300, Alexander Turenko wrote: > > Needed for #3276. > > Again, I'm not quite sure that you'll need this patch after you > rework the merger API so I'm not applying it until you send the > new API proposal. If we'll support iterator sources those helpers are convenient. All APIs we discussing now have them. IMHO, it is possible, but unlikely we'll decide to get rid of them. > > > --- > > src/lua/utils.c | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ > > src/lua/utils.h | 28 +++++++++++++++++++++ > > Some tests would be nice to have. I have added test/unit/luaL_iterator.c. I have to link many parts (*.a libraries) of tarantool to it and system dynamic libraries (dependencies of *.a), but I think size does not matter much here. This test found one error! An iterator can return zero count of values instead of returning nil and ipairs() behaves in that way. I fixed the implementation. > > > 2 files changed, 94 insertions(+) > > > > diff --git a/src/lua/utils.c b/src/lua/utils.c > > index eefb860ee..4d1eee6ab 100644 > > --- a/src/lua/utils.c > > +++ b/src/lua/utils.c > > @@ -969,6 +969,72 @@ luaT_state(void) > > return tarantool_L; > > } > > > > +/* {{{ Helper functions to interact with a Lua iterator from C */ > > + > > +struct luaL_iterator { > > + int gen; > > + int param; > > + int state; > > +}; > > + > > +struct luaL_iterator * > > +luaL_iterator_new_fromtable(lua_State *L, int idx) > > +{ > > + struct luaL_iterator *it = (struct luaL_iterator *) malloc( > > Nit: no need to convert void * to struct luaL_iterator *. Yep. Fixed. > > > + sizeof(struct luaL_iterator)); > > + > > + lua_rawgeti(L, idx, 1); /* Popped by luaL_ref(). */ > > + it->gen = luaL_ref(L, LUA_REGISTRYINDEX); > > + lua_rawgeti(L, idx, 2); /* Popped by luaL_ref(). */ > > + it->param = luaL_ref(L, LUA_REGISTRYINDEX); > > + lua_rawgeti(L, idx, 3); /* Popped by luaL_ref(). */ > > + it->state = luaL_ref(L, LUA_REGISTRYINDEX); > > + > > + return it; > > +} > > + > > +int > > +luaL_iterator_next(lua_State *L, struct luaL_iterator *it) > > +{ > > + int frame_start = lua_gettop(L); > > + > > + /* Call gen(param, state). */ > > + lua_rawgeti(L, LUA_REGISTRYINDEX, it->gen); > > + lua_rawgeti(L, LUA_REGISTRYINDEX, it->param); > > + lua_rawgeti(L, LUA_REGISTRYINDEX, it->state); > > + lua_call(L, 2, LUA_MULTRET); > > + int nresults = lua_gettop(L) - frame_start; > > + if (nresults == 0) { > > + luaL_error(L, "luaL_iterator_next: gen(param, state) must " > > + "return at least one result"); > > + unreachable(); > > + return 0; > > + } > > + > > + /* The call above returns nil as the first result. */ > > + if (lua_isnil(L, frame_start + 1)) { > > + lua_settop(L, frame_start); > > + return 0; > > + } > > + > > + /* Save the first result to it->state. */ > > + luaL_unref(L, LUA_REGISTRYINDEX, it->state); > > + lua_pushvalue(L, frame_start + 1); /* Popped by luaL_ref(). */ > > + it->state = luaL_ref(L, LUA_REGISTRYINDEX); > > + > > + return nresults; > > +} > > + > > +void luaL_iterator_free(lua_State *L, struct luaL_iterator *it) > > +{ > > + luaL_unref(L, LUA_REGISTRYINDEX, it->gen); > > + luaL_unref(L, LUA_REGISTRYINDEX, it->param); > > + luaL_unref(L, LUA_REGISTRYINDEX, it->state); > > + free(it); > > +} > > + > > +/* }}} */ > > + > > int > > tarantool_lua_utils_init(struct lua_State *L) > > { > > diff --git a/src/lua/utils.h b/src/lua/utils.h > > index bd302d8e9..6ba2e4767 100644 > > --- a/src/lua/utils.h > > +++ b/src/lua/utils.h > > @@ -525,6 +525,34 @@ luaL_checkfinite(struct lua_State *L, struct luaL_serializer *cfg, > > luaL_error(L, "number must not be NaN or Inf"); > > } > > > > +/* {{{ Helper functions to interact with a Lua iterator from C */ > > + > > +/** > > + * Holds iterator state (references to Lua objects). > > + */ > > +struct luaL_iterator; > > I'd make luaL_iterator struct transparent so that one could define it > on stack. > But luaL_iterator_new() do malloc and return a pointer. So we need to change the function or add another one to initialize a structure allocated outsize of the module. Can you please suggest me how the API should look if we'll make the structure transparent? I left it unchanged until we'll discuss this aspect. > > + > > +/** > > + * Create a Lua iterator from {gen, param, state}. > > May be, we could pass idx == 0 to create an iterator from > gen, param, state (without a table)? Would it be worthwhile? > I think it is good idea, because cases could be different. My thought was that we'll add another function for this case (if we'll need), but using idx == 0 is better. I created the similar API before for luaT_newtuple(). And I think the new merger API will require it. Added. > > + */ > > +struct luaL_iterator * > > +luaL_iterator_new_fromtable(lua_State *L, int idx); > > I don't think that _fromtable suffix is really necessary. > In light of the idx == 0 support, yep. Fixed. > > + > > +/** > > + * Move iterator to the next value. Push values returned by > > + * gen(param, state) and return its count. Zero means no more > > + * results available. > > + */ > > +int > > +luaL_iterator_next(lua_State *L, struct luaL_iterator *it); > > + > > +/** > > + * Free all resources held by the iterator. > > + */ > > +void luaL_iterator_free(lua_State *L, struct luaL_iterator *it); > > We usually match _new with _delete. Fixed. > > > + > > +/* }}} */ > > + > > int > > tarantool_lua_utils_init(struct lua_State *L); ---- diff --git a/src/lua/utils.c b/src/lua/utils.c index 4d1eee6ab..173d59a59 100644 --- a/src/lua/utils.c +++ b/src/lua/utils.c @@ -978,17 +978,30 @@ struct luaL_iterator { }; struct luaL_iterator * -luaL_iterator_new_fromtable(lua_State *L, int idx) +luaL_iterator_new(lua_State *L, int idx) { - struct luaL_iterator *it = (struct luaL_iterator *) malloc( - sizeof(struct luaL_iterator)); - - lua_rawgeti(L, idx, 1); /* Popped by luaL_ref(). */ - it->gen = luaL_ref(L, LUA_REGISTRYINDEX); - lua_rawgeti(L, idx, 2); /* Popped by luaL_ref(). */ - it->param = luaL_ref(L, LUA_REGISTRYINDEX); - lua_rawgeti(L, idx, 3); /* Popped by luaL_ref(). */ - it->state = luaL_ref(L, LUA_REGISTRYINDEX); + struct luaL_iterator *it = malloc(sizeof(struct luaL_iterator)); + + if (idx == 0) { + /* gen, param, state are on top of a Lua stack. */ + lua_pushvalue(L, -3); /* Popped by luaL_ref(). */ + it->gen = luaL_ref(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, -2); /* Popped by luaL_ref(). */ + it->param = luaL_ref(L, LUA_REGISTRYINDEX); + lua_pushvalue(L, -1); /* Popped by luaL_ref(). */ + it->state = luaL_ref(L, LUA_REGISTRYINDEX); + } else { + /* + * {gen, param, state} table is at idx in a Lua + * stack. + */ + lua_rawgeti(L, idx, 1); /* Popped by luaL_ref(). */ + it->gen = luaL_ref(L, LUA_REGISTRYINDEX); + lua_rawgeti(L, idx, 2); /* Popped by luaL_ref(). */ + it->param = luaL_ref(L, LUA_REGISTRYINDEX); + lua_rawgeti(L, idx, 3); /* Popped by luaL_ref(). */ + it->state = luaL_ref(L, LUA_REGISTRYINDEX); + } return it; } @@ -1004,15 +1017,15 @@ luaL_iterator_next(lua_State *L, struct luaL_iterator *it) lua_rawgeti(L, LUA_REGISTRYINDEX, it->state); lua_call(L, 2, LUA_MULTRET); int nresults = lua_gettop(L) - frame_start; - if (nresults == 0) { - luaL_error(L, "luaL_iterator_next: gen(param, state) must " - "return at least one result"); - unreachable(); - return 0; - } - /* The call above returns nil as the first result. */ - if (lua_isnil(L, frame_start + 1)) { + /* + * gen() function can either return nil when the iterator + * ends or return zero count of values. + * + * In LuaJIT pairs() returns nil, but ipairs() returns + * nothing when ends. + */ + if (nresults == 0 || lua_isnil(L, frame_start + 1)) { lua_settop(L, frame_start); return 0; } @@ -1025,7 +1038,7 @@ luaL_iterator_next(lua_State *L, struct luaL_iterator *it) return nresults; } -void luaL_iterator_free(lua_State *L, struct luaL_iterator *it) +void luaL_iterator_delete(lua_State *L, struct luaL_iterator *it) { luaL_unref(L, LUA_REGISTRYINDEX, it->gen); luaL_unref(L, LUA_REGISTRYINDEX, it->param); diff --git a/src/lua/utils.h b/src/lua/utils.h index 6ba2e4767..2df2f5015 100644 --- a/src/lua/utils.h +++ b/src/lua/utils.h @@ -533,10 +533,16 @@ luaL_checkfinite(struct lua_State *L, struct luaL_serializer *cfg, struct luaL_iterator; /** - * Create a Lua iterator from {gen, param, state}. + * Create a Lua iterator from a gen, param, state triplet. + * + * If idx == 0, then three top stack values are used as the + * triplet. + * + * Otherwise idx is index on Lua stack points to a + * {gen, param, state} table. */ struct luaL_iterator * -luaL_iterator_new_fromtable(lua_State *L, int idx); +luaL_iterator_new(lua_State *L, int idx); /** * Move iterator to the next value. Push values returned by @@ -549,7 +555,7 @@ luaL_iterator_next(lua_State *L, struct luaL_iterator *it); /** * Free all resources held by the iterator. */ -void luaL_iterator_free(lua_State *L, struct luaL_iterator *it); +void luaL_iterator_delete(lua_State *L, struct luaL_iterator *it); /* }}} */ diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 0025d3611..c2c45a4b8 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -138,6 +138,10 @@ add_executable(histogram.test histogram.c) target_link_libraries(histogram.test stat unit) add_executable(ratelimit.test ratelimit.c) target_link_libraries(ratelimit.test unit) +add_executable(luaL_iterator.test luaL_iterator.c) +target_link_libraries(luaL_iterator.test unit server core misc + ${CURL_LIBRARIES} ${LIBYAML_LIBRARIES} ${READLINE_LIBRARIES} + ${ICU_LIBRARIES} ${LUAJIT_LIBRARIES}) add_executable(say.test say.c) target_link_libraries(say.test core unit) diff --git a/test/unit/luaL_iterator.c b/test/unit/luaL_iterator.c new file mode 100644 index 000000000..5a254f27d --- /dev/null +++ b/test/unit/luaL_iterator.c @@ -0,0 +1,159 @@ +#include <lua.h> /* lua_*() */ +#include <lauxlib.h> /* luaL_*() */ +#include <lualib.h> /* luaL_openlibs() */ +#include "unit.h" /* plan, header, footer, is */ +#include "lua/utils.h" /* luaL_iterator_*() */ + +extern char fun_lua[]; + +int +main() +{ + struct { + /* A string to output with a test case. */ + const char *description; + /* A string with Lua code to push an iterator. */ + const char *init; + /* + * How much values are pushed by the Lua code + * above. + */ + int init_retvals; + /* + * Start values from this number to distinguish + * them from its ordinal. + */ + int first_value; + /* + * Lua stack index where {gen, param, state} is + * placed or zero. + */ + int idx; + /* How much values are in the iterator. */ + int iterations; + } cases[] = { + { + .description = "pairs, zero idx", + .init = "return pairs({42})", + .init_retvals = 3, + .first_value = 42, + .idx = 0, + .iterations = 1, + }, + { + .description = "ipairs, zero idx", + .init = "return ipairs({42, 43, 44})", + .init_retvals = 3, + .first_value = 42, + .idx = 0, + .iterations = 3, + }, + { + .description = "luafun iterator, zero idx", + .init = "return fun.wrap(ipairs({42, 43, 44}))", + .init_retvals = 3, + .first_value = 42, + .idx = 0, + .iterations = 3, + }, + { + .description = "pairs, from table", + .init = "return {pairs({42})}", + .init_retvals = 1, + .first_value = 42, + .idx = -1, + .iterations = 1, + }, + { + .description = "ipairs, from table", + .init = "return {ipairs({42, 43, 44})}", + .init_retvals = 1, + .first_value = 42, + .idx = -1, + .iterations = 3, + }, + { + .description = "luafun iterator, from table", + .init = "return {fun.wrap(ipairs({42, 43, 44}))}", + .init_retvals = 1, + .first_value = 42, + .idx = -1, + .iterations = 3, + }, + }; + + int cases_cnt = (int) (sizeof(cases) / sizeof(cases[0])); + /* + * * Check stack size after creating luaL_iterator (triple + * times). + * * 4 checks per iteration. + * * Check that values ends. + */ + int planned = 0; + for (int i = 0; i < cases_cnt; ++i) + planned += cases[i].iterations * 4 + 4; + + plan(planned); + header(); + + struct lua_State *L = luaL_newstate(); + luaL_openlibs(L); + + /* + * Expose luafun. + * + * Don't register it in package.loaded for simplicity. + */ + luaL_loadstring(L, fun_lua); + lua_call(L, 0, 1); + lua_setglobal(L, "fun"); + + for (int i = 0; i < cases_cnt; ++i) { + const char *description = cases[i].description; + int top = lua_gettop(L); + + /* Push an iterator to the Lua stack. */ + luaL_loadstring(L, cases[i].init); + lua_call(L, 0, cases[i].init_retvals); + + /* Create the luaL_iterator structure. */ + struct luaL_iterator *it = luaL_iterator_new(L, cases[i].idx); + lua_pop(L, cases[i].init_retvals); + + /* Check stack size. */ + is(lua_gettop(L) - top, 0, "%s: stack size", description); + + /* Iterate over values and check them. */ + for (int j = 0; j < cases[i].iterations; ++j) { + int top = lua_gettop(L); + int rc = luaL_iterator_next(L, it); + is(rc, 2, "%s: iter %d: gen() retval count", + description, j); + is(luaL_checkinteger(L, -2), j + 1, + "%s: iter %d: gen() 1st retval", + description, j); + is(luaL_checkinteger(L, -1), j + cases[i].first_value, + "%s: iter %d: gen() 2nd retval", + description, j); + lua_pop(L, 2); + is(lua_gettop(L) - top, 0, "%s: iter: %d: stack size", + description, j); + } + + /* Check the iterator ends when expected. */ + int rc = luaL_iterator_next(L, it); + is(rc, 0, "%s: iterator ends", description); + + /* Check stack size. */ + is(lua_gettop(L) - top, 0, "%s: stack size", description); + + /* Free the luaL_iterator structure. */ + luaL_iterator_delete(L, it); + + /* Check stack size. */ + is(lua_gettop(L) - top, 0, "%s: stack size", description); + } + + footer(); + return check_plan(); +} diff --git a/test/unit/luaL_iterator.result b/test/unit/luaL_iterator.result new file mode 100644 index 000000000..f4eda5695 --- /dev/null +++ b/test/unit/luaL_iterator.result @@ -0,0 +1,83 @@ +1..80 + *** main *** +ok 1 - pairs, zero idx: stack size +ok 2 - pairs, zero idx: iter 0: gen() retval count +ok 3 - pairs, zero idx: iter 0: gen() 1st retval +ok 4 - pairs, zero idx: iter 0: gen() 2nd retval +ok 5 - pairs, zero idx: iter: 0: stack size +ok 6 - pairs, zero idx: iterator ends +ok 7 - pairs, zero idx: stack size +ok 8 - pairs, zero idx: stack size +ok 9 - ipairs, zero idx: stack size +ok 10 - ipairs, zero idx: iter 0: gen() retval count +ok 11 - ipairs, zero idx: iter 0: gen() 1st retval +ok 12 - ipairs, zero idx: iter 0: gen() 2nd retval +ok 13 - ipairs, zero idx: iter: 0: stack size +ok 14 - ipairs, zero idx: iter 1: gen() retval count +ok 15 - ipairs, zero idx: iter 1: gen() 1st retval +ok 16 - ipairs, zero idx: iter 1: gen() 2nd retval +ok 17 - ipairs, zero idx: iter: 1: stack size +ok 18 - ipairs, zero idx: iter 2: gen() retval count +ok 19 - ipairs, zero idx: iter 2: gen() 1st retval +ok 20 - ipairs, zero idx: iter 2: gen() 2nd retval +ok 21 - ipairs, zero idx: iter: 2: stack size +ok 22 - ipairs, zero idx: iterator ends +ok 23 - ipairs, zero idx: stack size +ok 24 - ipairs, zero idx: stack size +ok 25 - luafun iterator, zero idx: stack size +ok 26 - luafun iterator, zero idx: iter 0: gen() retval count +ok 27 - luafun iterator, zero idx: iter 0: gen() 1st retval +ok 28 - luafun iterator, zero idx: iter 0: gen() 2nd retval +ok 29 - luafun iterator, zero idx: iter: 0: stack size +ok 30 - luafun iterator, zero idx: iter 1: gen() retval count +ok 31 - luafun iterator, zero idx: iter 1: gen() 1st retval +ok 32 - luafun iterator, zero idx: iter 1: gen() 2nd retval +ok 33 - luafun iterator, zero idx: iter: 1: stack size +ok 34 - luafun iterator, zero idx: iter 2: gen() retval count +ok 35 - luafun iterator, zero idx: iter 2: gen() 1st retval +ok 36 - luafun iterator, zero idx: iter 2: gen() 2nd retval +ok 37 - luafun iterator, zero idx: iter: 2: stack size +ok 38 - luafun iterator, zero idx: iterator ends +ok 39 - luafun iterator, zero idx: stack size +ok 40 - luafun iterator, zero idx: stack size +ok 41 - pairs, from table: stack size +ok 42 - pairs, from table: iter 0: gen() retval count +ok 43 - pairs, from table: iter 0: gen() 1st retval +ok 44 - pairs, from table: iter 0: gen() 2nd retval +ok 45 - pairs, from table: iter: 0: stack size +ok 46 - pairs, from table: iterator ends +ok 47 - pairs, from table: stack size +ok 48 - pairs, from table: stack size +ok 49 - ipairs, from table: stack size +ok 50 - ipairs, from table: iter 0: gen() retval count +ok 51 - ipairs, from table: iter 0: gen() 1st retval +ok 52 - ipairs, from table: iter 0: gen() 2nd retval +ok 53 - ipairs, from table: iter: 0: stack size +ok 54 - ipairs, from table: iter 1: gen() retval count +ok 55 - ipairs, from table: iter 1: gen() 1st retval +ok 56 - ipairs, from table: iter 1: gen() 2nd retval +ok 57 - ipairs, from table: iter: 1: stack size +ok 58 - ipairs, from table: iter 2: gen() retval count +ok 59 - ipairs, from table: iter 2: gen() 1st retval +ok 60 - ipairs, from table: iter 2: gen() 2nd retval +ok 61 - ipairs, from table: iter: 2: stack size +ok 62 - ipairs, from table: iterator ends +ok 63 - ipairs, from table: stack size +ok 64 - ipairs, from table: stack size +ok 65 - luafun iterator, from table: stack size +ok 66 - luafun iterator, from table: iter 0: gen() retval count +ok 67 - luafun iterator, from table: iter 0: gen() 1st retval +ok 68 - luafun iterator, from table: iter 0: gen() 2nd retval +ok 69 - luafun iterator, from table: iter: 0: stack size +ok 70 - luafun iterator, from table: iter 1: gen() retval count +ok 71 - luafun iterator, from table: iter 1: gen() 1st retval +ok 72 - luafun iterator, from table: iter 1: gen() 2nd retval +ok 73 - luafun iterator, from table: iter: 1: stack size +ok 74 - luafun iterator, from table: iter 2: gen() retval count +ok 75 - luafun iterator, from table: iter 2: gen() 1st retval +ok 76 - luafun iterator, from table: iter 2: gen() 2nd retval +ok 77 - luafun iterator, from table: iter: 2: stack size +ok 78 - luafun iterator, from table: iterator ends +ok 79 - luafun iterator, from table: stack size +ok 80 - luafun iterator, from table: stack size + *** main: done ***
next prev parent reply other threads:[~2019-01-15 23:26 UTC|newest] Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top 2019-01-09 20:20 [PATCH v2 0/6] Merger Alexander Turenko 2019-01-09 20:20 ` [PATCH v2 1/6] Add luaL_iscallable with support of cdata metatype Alexander Turenko 2019-01-10 12:21 ` Vladimir Davydov 2019-01-09 20:20 ` [PATCH v2 2/6] Add functions to ease using Lua iterators from C Alexander Turenko 2019-01-10 12:29 ` Vladimir Davydov 2019-01-15 23:26 ` Alexander Turenko [this message] 2019-01-16 8:18 ` Vladimir Davydov 2019-01-16 11:40 ` Alexander Turenko 2019-01-16 12:20 ` Vladimir Davydov 2019-01-17 1:20 ` Alexander Turenko 2019-01-28 18:17 ` Alexander Turenko 2019-03-01 4:04 ` Alexander Turenko 2019-01-09 20:20 ` [PATCH v2 3/6] lua: add luaT_newtuple() Alexander Turenko 2019-01-10 12:44 ` Vladimir Davydov 2019-01-18 21:58 ` Alexander Turenko 2019-01-23 16:12 ` Vladimir Davydov 2019-01-28 16:51 ` Alexander Turenko 2019-03-01 4:08 ` Alexander Turenko 2019-01-09 20:20 ` [PATCH v2 4/6] lua: add luaT_new_key_def() Alexander Turenko 2019-01-10 13:07 ` Vladimir Davydov 2019-01-29 18:52 ` Alexander Turenko 2019-01-30 10:58 ` Alexander Turenko 2019-03-01 4:10 ` Alexander Turenko 2019-01-09 20:20 ` [PATCH v2 5/6] net.box: add helpers to decode msgpack headers Alexander Turenko 2019-01-10 17:29 ` Vladimir Davydov 2019-02-01 15:11 ` Alexander Turenko 2019-03-05 12:00 ` Alexander Turenko 2019-01-09 20:20 ` [PATCH v2 6/6] Add merger for tuple streams Alexander Turenko
Reply instructions: You may reply publicly to this message via plain-text email using any one of the following methods: * Save the following mbox file, import it into your mail client, and reply-to-all from there: mbox Avoid top-posting and favor interleaved quoting: https://en.wikipedia.org/wiki/Posting_style#Interleaved_style * Reply using the --to, --cc, and --in-reply-to switches of git-send-email(1): git send-email \ --in-reply-to=20190115232623.huxpqj4adoqqi3mo@tkn_work_nb \ --to=alexander.turenko@tarantool.org \ --cc=tarantool-patches@freelists.org \ --cc=vdavydov.dev@gmail.com \ --subject='Re: [PATCH v2 2/6] Add functions to ease using Lua iterators from C' \ /path/to/YOUR_REPLY https://kernel.org/pub/software/scm/git/docs/git-send-email.html * If your mail client supports setting the In-Reply-To header via mailto: links, try the mailto: link
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox