Message ID | 20200623152505.GI1435482@coredump.intra.peff.net (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | fast-export: allow seeding the anonymized mapping | expand |
On Tue, Jun 23, 2020 at 11:25 AM Jeff King <peff@peff.net> wrote: > diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt > @@ -238,6 +243,25 @@ collapse "User 0", "User 1", etc into "User X"). This produces a much > +[...] For example, if you have a bug which reproduces > +with `git rev-list mybranch -- foo.c`, you can run: > + > +--------------------------------------------------- > +$ git fast-export --anonymize --all \ > + --seed-anonymized=foo.c:secret.c \ > + --seed-anonymized=mybranch \ > + >stream > +--------------------------------------------------- > + > +After importing the stream, you can then run `git rev-list mybranch -- > +secret.c` in the anonymized repository. I understand that your intention here is to demonstrate both forms of --seed-anonymized, but I'm slightly concerned that people may interpret this example as meaning that you are not allowed to anonymize the refname when anonymizing a pathname. It might be less ambiguous to avoid the "short form" in the example; people who have read the description of --seed-anonymized will know that the short form can be used without having to see it in an example. > +Note that paths and refnames are split into tokens at slash boundaries. > +The command above would anonymize `subdir/foo.c` as something like > +`path123/secret.c`. Confusing. This seems to be saying that anonymizing filenames in subdirectories is pointless because you can't know how the leading directory names will be anonymized. That leaves the reader wondering how to deal with the situation. Does it require using --seed-anonymized for each path component leading up to the filename? Or can --seed-anonymized take an full pathname (leading directory components and filename) in one shot? > @@ -168,8 +169,18 @@ static const char *anonymize_str(struct hashmap *map, > - ret = hashmap_get_entry(map, &key, hash, &key); > > + /* First check if it's a token the user configured manually... */ > + if (anonymized_seeds.cmpfn) > + ret = hashmap_get_entry(&anonymized_seeds, &key, hash, &key); > + else > + ret = NULL; > + > + /* ...otherwise check if we've already seen it in this context... */ > + if (!ret) > + ret = hashmap_get_entry(map, &key, hash, &key); > + > + /* ...and finally generate a new mapping if necessary */ I was a bit surprised to see that --seed-anonymized values are stored in a separate hash map rather than simply being used to (literally) seed the existing anonymization hash map. I guess there's a good technical reason for doing it this way, such as the normal anonymization hash map not yet being in existence at the time the --seed-anonymized option is processed? (I haven't checked because I'm too lazy, so it may not be worth spending time answering me.) > @@ -1188,6 +1230,9 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) > OPT_BOOL(0, "anonymize", &anonymize, N_("anonymize output")), > + OPT_CALLBACK_F(0, "seed-anonymized", &anonymized_seeds, N_("from:to"), > + N_("convert <from> to <to> in anonymized output"), > + PARSE_OPT_NONEG, parse_opt_seed_anonymized), Would it be worthwhile to add a check somewhere after the parse_options() invocation and complain if --seed-anonymized was used without --anonymize? (Or should --seed-anonymized perhaps imply --anonymize?)
On Tue, Jun 23, 2020 at 11:25 AM Jeff King <peff@peff.net> wrote: > Let's make it possible to seed the anonymization map. This lets users > either: > [...] > Signed-off-by: Jeff King <peff@peff.net> > --- > diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt > @@ -119,6 +119,11 @@ by keeping the marks the same across runs. > +--seed-anonymized=<from>[:<to>]:: > + Convert token `<from>` to `<to>` in the anonymized output. If > + `<to>` is omitted, map `<from>` to itself (i.e., do not > + anonymize it). See the section on `ANONYMIZING` below. By the way (possible bikeshedding ahead), "seed anonymous" seems overly technical. I wonder if a name such as '--anonymize-to=<from>[:<to>]' might be clearer and easier for people to understand. In fact, in an earlier email, I asked whether --seed-anonymized should imply --anonymize. Thinking further on this, I wonder if we even need the second option name. It should be possible to overload the existing --anonymize to handle all functions. For instance: '--anonymize' would anonymize everything '--anonymize=<from>[:<to>]' would anonymize and map <from> to <to> So, the example you give in the documentation would become: git fast-export --all \ --anonymize=foo.c:secret.c \ --anonymize=mybranch >stream Or is that too cryptic?
On Tue, Jun 23, 2020 at 01:16:05PM -0400, Eric Sunshine wrote: > On Tue, Jun 23, 2020 at 11:25 AM Jeff King <peff@peff.net> wrote: > > diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt > > @@ -238,6 +243,25 @@ collapse "User 0", "User 1", etc into "User X"). This produces a much > > +[...] For example, if you have a bug which reproduces > > +with `git rev-list mybranch -- foo.c`, you can run: > > + > > +--------------------------------------------------- > > +$ git fast-export --anonymize --all \ > > + --seed-anonymized=foo.c:secret.c \ > > + --seed-anonymized=mybranch \ > > + >stream > > +--------------------------------------------------- > > + > > +After importing the stream, you can then run `git rev-list mybranch -- > > +secret.c` in the anonymized repository. > > I understand that your intention here is to demonstrate both forms of > --seed-anonymized, but I'm slightly concerned that people may > interpret this example as meaning that you are not allowed to > anonymize the refname when anonymizing a pathname. It might be less > ambiguous to avoid the "short form" in the example; people who have > read the description of --seed-anonymized will know that the short > form can be used without having to see it in an example. I'm not sure what you'd write, then. You can't mention "mybranch" anymore if it was anonymized. Are you suggesting to make the example: git rev-list -- foo.c by itself? > > +Note that paths and refnames are split into tokens at slash boundaries. > > +The command above would anonymize `subdir/foo.c` as something like > > +`path123/secret.c`. > > Confusing. This seems to be saying that anonymizing filenames in > subdirectories is pointless because you can't know how the leading > directory names will be anonymized. That leaves the reader wondering > how to deal with the situation. Does it require using > --seed-anonymized for each path component leading up to the filename? You can do that, but I think it would be simpler to just find "secret.c" in the anonymized repo (either in the checkout, or just "git ls-tree -r"). > Or can --seed-anonymized take an full pathname (leading directory > components and filename) in one shot? No, it can't. Suggested wording? That's what I was trying to say with the above sentence. > > + /* First check if it's a token the user configured manually... */ > > + if (anonymized_seeds.cmpfn) > > + ret = hashmap_get_entry(&anonymized_seeds, &key, hash, &key); > > + else > > + ret = NULL; > > + > > + /* ...otherwise check if we've already seen it in this context... */ > > + if (!ret) > > + ret = hashmap_get_entry(map, &key, hash, &key); > > + > > + /* ...and finally generate a new mapping if necessary */ > > I was a bit surprised to see that --seed-anonymized values are stored > in a separate hash map rather than simply being used to (literally) > seed the existing anonymization hash map. I guess there's a good > technical reason for doing it this way, such as the normal > anonymization hash map not yet being in existence at the time the > --seed-anonymized option is processed? (I haven't checked because I'm > too lazy, so it may not be worth spending time answering me.) The reason is that there isn't one anonymization hash map. There's a separate one for each generator (so refs become "refs/heads/ref123" and paths become "path123/path456"). > > @@ -1188,6 +1230,9 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) > > OPT_BOOL(0, "anonymize", &anonymize, N_("anonymize output")), > > + OPT_CALLBACK_F(0, "seed-anonymized", &anonymized_seeds, N_("from:to"), > > + N_("convert <from> to <to> in anonymized output"), > > + PARSE_OPT_NONEG, parse_opt_seed_anonymized), > > Would it be worthwhile to add a check somewhere after the > parse_options() invocation and complain if --seed-anonymized was used > without --anonymize? (Or should --seed-anonymized perhaps imply > --anonymize?) I thought about implying, but I have a slight preference to err on the side of making things less magical. I don't mind triggering a warning or error, but it's not like anything _bad_ happens if you don't say --anonymize. It just doesn't do anything, which seems like a perfectly logical outcome. -Peff
On Tue, Jun 23, 2020 at 02:11:51PM -0400, Eric Sunshine wrote: > On Tue, Jun 23, 2020 at 11:25 AM Jeff King <peff@peff.net> wrote: > > Let's make it possible to seed the anonymization map. This lets users > > either: > > [...] > > Signed-off-by: Jeff King <peff@peff.net> > > --- > > diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt > > @@ -119,6 +119,11 @@ by keeping the marks the same across runs. > > +--seed-anonymized=<from>[:<to>]:: > > + Convert token `<from>` to `<to>` in the anonymized output. If > > + `<to>` is omitted, map `<from>` to itself (i.e., do not > > + anonymize it). See the section on `ANONYMIZING` below. > > By the way (possible bikeshedding ahead), "seed anonymous" seems > overly technical. I wonder if a name such as > '--anonymize-to=<from>[:<to>]' might be clearer and easier for people > to understand. I wrestled with the name, and I agree "seed" is overly technical. And I came up with many similar variations of "anonymize-to", but they all seemed ambiguous (e.g., it could be "to" a file that we're storing the data in). Perhaps "--anonymize-map" would be less technical? > In fact, in an earlier email, I asked whether --seed-anonymized should > imply --anonymize. Thinking further on this, I wonder if we even need > the second option name. It should be possible to overload the existing > --anonymize to handle all functions. For instance: > > '--anonymize' would anonymize everything > > '--anonymize=<from>[:<to>]' would anonymize and map <from> to <to> > > So, the example you give in the documentation would become: > > git fast-export --all \ > --anonymize=foo.c:secret.c \ > --anonymize=mybranch >stream > > Or is that too cryptic? Yeah, that was another one I considered, but it both seemed cryptic (after all, we're saying what _not_ to anonymize), and it squats on the "anonymize" option. So imagine we had another option later, like "anonymize blobs and paths, but not refs", that could easily be "--anonymize=blobs,path" or "--anonymize=!refs". I'd rather not paint ourselves in a corner. -Peff
On Tue, Jun 23, 2020 at 2:31 PM Jeff King <peff@peff.net> wrote: > On Tue, Jun 23, 2020 at 01:16:05PM -0400, Eric Sunshine wrote: > > On Tue, Jun 23, 2020 at 11:25 AM Jeff King <peff@peff.net> wrote: > > I understand that your intention here is to demonstrate both forms of > > --seed-anonymized, but I'm slightly concerned that people may > > interpret this example as meaning that you are not allowed to > > anonymize the refname when anonymizing a pathname. It might be less > > ambiguous to avoid the "short form" in the example; people who have > > read the description of --seed-anonymized will know that the short > > form can be used without having to see it in an example. > > I'm not sure what you'd write, then. You can't mention "mybranch" > anymore if it was anonymized. Are you suggesting to make the example: > > git rev-list -- foo.c > > by itself? Sorry, I meant to provide an example like this: For example, if you have a bug which reproduces with `git rev-list sensitive -- secret.c`, you can run: $ git fast-export --anonymize --all \ --seed-anonymized=sensitive:foo \ --seed-anonymized=secret.c:bar.c \ >stream After importing the stream, you can then run `git rev-list foo -- bar.c` in the anonymized repository. > > > +Note that paths and refnames are split into tokens at slash boundaries. > > > +The command above would anonymize `subdir/foo.c` as something like > > > +`path123/secret.c`. > > > > Confusing. This seems to be saying that anonymizing filenames in > > subdirectories is pointless because you can't know how the leading > > directory names will be anonymized. That leaves the reader wondering > > how to deal with the situation. Does it require using > > --seed-anonymized for each path component leading up to the filename? > > You can do that, but I think it would be simpler to just find "secret.c" > in the anonymized repo (either in the checkout, or just "git ls-tree > -r"). > > > Or can --seed-anonymized take an full pathname (leading directory > > components and filename) in one shot? > > No, it can't. Suggested wording? That's what I was trying to say with > the above sentence. Hmm, perhaps your original attempt can be extended slightly to state it more explicitly? Note that paths and refnames are split into tokens at slash boundaries. The command above would anonymize `subdir/foo.c` as something like `path123/secret.c`; you could then search for `secret.c` in the anonymized repository to determine the final pathname. To make referencing the final pathname simpler, you can seed anonymization for each path component; so, if you also anonymize `subdir` to `publicdir`, then the final pathname would be `publicdir/secret.c`. This makes me wonder if --seed-anonymized should do its own tokenization so that --seed-anonymized=subdir/foo:public/bar is automatically understood as anonymizing "subdir" to "public" _and_ "foo" to "bar". But that potentially gets weird if you say: --seed-anonymized=a/b:q/p --seed-anonymized=a/c:y/z in which case you've given conflicting replacements for "a". (I suppose it could issue a warning message in that case.) > > Would it be worthwhile to add a check somewhere after the > > parse_options() invocation and complain if --seed-anonymized was used > > without --anonymize? (Or should --seed-anonymized perhaps imply > > --anonymize?) > > I thought about implying, but I have a slight preference to err on the > side of making things less magical. I don't mind triggering a warning or > error, but it's not like anything _bad_ happens if you don't say > --anonymize. It just doesn't do anything, which seems like a perfectly > logical outcome. Lack of a warning or error could be kind of bad if the person doesn't check the fast-export file before sending it out and only discovers later that: git fast-export --seed-anonymized=foo:bar didn't perform _any_ anonymization at all.
On Tue, Jun 23, 2020 at 2:35 PM Jeff King <peff@peff.net> wrote: > On Tue, Jun 23, 2020 at 02:11:51PM -0400, Eric Sunshine wrote: > > By the way (possible bikeshedding ahead), "seed anonymous" seems > > overly technical. I wonder if a name such as > > '--anonymize-to=<from>[:<to>]' might be clearer and easier for people > > to understand. > > I wrestled with the name, and I agree "seed" is overly technical. And I > came up with many similar variations of "anonymize-to", but they all > seemed ambiguous (e.g., it could be "to" a file that we're storing the > data in). > > Perhaps "--anonymize-map" would be less technical? That's not too bad. It is better than --seed-anonymized. I haven't come up with any name which improves upon it. > > In fact, in an earlier email, I asked whether --seed-anonymized should > > imply --anonymize. Thinking further on this, I wonder if we even need > > the second option name. It should be possible to overload the existing > > --anonymize to handle all functions. For instance: > > > > git fast-export --all \ > > --anonymize=foo.c:secret.c \ > > --anonymize=mybranch >stream > > > > Or is that too cryptic? > > Yeah, that was another one I considered, but it both seemed cryptic > (after all, we're saying what _not_ to anonymize), and it squats on the > "anonymize" option. So imagine we had another option later, like > "anonymize blobs and paths, but not refs", that could easily be > "--anonymize=blobs,path" or "--anonymize=!refs". I'd rather not paint > ourselves in a corner. Okay, makes sense.
On Tue, Jun 23, 2020 at 04:30:23PM -0400, Eric Sunshine wrote: > > I'm not sure what you'd write, then. You can't mention "mybranch" > > anymore if it was anonymized. Are you suggesting to make the example: > > > > git rev-list -- foo.c > > > > by itself? > > Sorry, I meant to provide an example like this: > > For example, if you have a bug which reproduces with `git rev-list > sensitive -- secret.c`, you can run: > > $ git fast-export --anonymize --all \ > --seed-anonymized=sensitive:foo \ > --seed-anonymized=secret.c:bar.c \ > >stream > > After importing the stream, you can then run `git rev-list foo -- > bar.c` in the anonymized repository. Thanks, that makes sense. I took this as-is for my reroll (modulo the change of option name discussed elsewhere). > Hmm, perhaps your original attempt can be extended slightly to state > it more explicitly? > > Note that paths and refnames are split into tokens at slash > boundaries. The command above would anonymize `subdir/foo.c` as > something like `path123/secret.c`; you could then search for > `secret.c` in the anonymized repository to determine the final > pathname. > > To make referencing the final pathname simpler, you can seed > anonymization for each path component; so, if you also anonymize > `subdir` to `publicdir`, then the final pathname would be > `publicdir/secret.c`. Thanks, I took this modulo some fixups to match the example above, and to avoid the use of the word "seed" based on our other discussion. > This makes me wonder if --seed-anonymized should do its own > tokenization so that --seed-anonymized=subdir/foo:public/bar is > automatically understood as anonymizing "subdir" to "public" _and_ > "foo" to "bar". But that potentially gets weird if you say: > > --seed-anonymized=a/b:q/p --seed-anonymized=a/c:y/z > > in which case you've given conflicting replacements for "a". (I > suppose it could issue a warning message in that case.) Right, I think you get into weird corner cases. Another issue is that not all items are tokenized (e.g., if your author name was foo/bar, you'd want that replaced as a whole). Probably you could add both the broken-down and full inputs. Yet another issue is that you can't add a token with a ":" due to the syntax. This is an infrequently-enough-used feature that I think it's worth keeping things simple, even if they're a little less convenient to invoke. > Lack of a warning or error could be kind of bad if the person doesn't > check the fast-export file before sending it out and only discovers > later that: > > git fast-export --seed-anonymized=foo:bar > > didn't perform _any_ anonymization at all. Good point. I'd hope people would glance at the output before sending it out, but given that it's a potential safety issue, it probably is worth detecting this case. I'll add it to my re-roll. -Peff
On Tue, Jun 23, 2020 at 04:35:37PM -0400, Eric Sunshine wrote: > > I wrestled with the name, and I agree "seed" is overly technical. And I > > came up with many similar variations of "anonymize-to", but they all > > seemed ambiguous (e.g., it could be "to" a file that we're storing the > > data in). > > > > Perhaps "--anonymize-map" would be less technical? > > That's not too bad. It is better than --seed-anonymized. I haven't > come up with any name which improves upon it. I went with that in my reroll, and avoided using the word "seed" at all in the documentation (I did keep the name "anonymized_seeds" as the internal variable name for the hashmap, since just calling it "map" there is ambiguous with all of the other maps). -Peff
diff --git a/Documentation/git-fast-export.txt b/Documentation/git-fast-export.txt index e8950de3ba..2d7b62e835 100644 --- a/Documentation/git-fast-export.txt +++ b/Documentation/git-fast-export.txt @@ -119,6 +119,11 @@ by keeping the marks the same across runs. the shape of the history and stored tree. See the section on `ANONYMIZING` below. +--seed-anonymized=<from>[:<to>]:: + Convert token `<from>` to `<to>` in the anonymized output. If + `<to>` is omitted, map `<from>` to itself (i.e., do not + anonymize it). See the section on `ANONYMIZING` below. + --reference-excluded-parents:: By default, running a command such as `git fast-export master~5..master` will not include the commit master{tilde}5 @@ -238,6 +243,25 @@ collapse "User 0", "User 1", etc into "User X"). This produces a much smaller output, and it is usually easy to quickly confirm that there is no private data in the stream. +Reproducing some bugs may require referencing particular commits or +paths, which becomes challenging after refnames and paths have been +anonymized. You can ask for a particular token to be left as-is or +mapped to a new value. For example, if you have a bug which reproduces +with `git rev-list mybranch -- foo.c`, you can run: + +--------------------------------------------------- +$ git fast-export --anonymize --all \ + --seed-anonymized=foo.c:secret.c \ + --seed-anonymized=mybranch \ + >stream +--------------------------------------------------- + +After importing the stream, you can then run `git rev-list mybranch -- +secret.c` in the anonymized repository. + +Note that paths and refnames are split into tokens at slash boundaries. +The command above would anonymize `subdir/foo.c` as something like +`path123/secret.c`. LIMITATIONS ----------- diff --git a/builtin/fast-export.c b/builtin/fast-export.c index 1cbca5b4b4..ef82497bbf 100644 --- a/builtin/fast-export.c +++ b/builtin/fast-export.c @@ -45,6 +45,7 @@ static struct string_list extra_refs = STRING_LIST_INIT_NODUP; static struct string_list tag_refs = STRING_LIST_INIT_NODUP; static struct refspec refspecs = REFSPEC_INIT_FETCH; static int anonymize; +static struct hashmap anonymized_seeds; static struct revision_sources revision_sources; static int parse_opt_signed_tag_mode(const struct option *opt, @@ -168,8 +169,18 @@ static const char *anonymize_str(struct hashmap *map, hashmap_entry_init(&key.hash, memhash(orig, len)); key.orig = orig; key.orig_len = len; - ret = hashmap_get_entry(map, &key, hash, &key); + /* First check if it's a token the user configured manually... */ + if (anonymized_seeds.cmpfn) + ret = hashmap_get_entry(&anonymized_seeds, &key, hash, &key); + else + ret = NULL; + + /* ...otherwise check if we've already seen it in this context... */ + if (!ret) + ret = hashmap_get_entry(map, &key, hash, &key); + + /* ...and finally generate a new mapping if necessary */ if (!ret) { FLEX_ALLOC_MEM(ret, orig, orig, len); hashmap_entry_init(&ret->hash, key.hash.hash); @@ -1147,6 +1158,37 @@ static void handle_deletes(void) } } +static char *anonymize_seed(void *data) +{ + return xstrdup(data); +} + +static int parse_opt_seed_anonymized(const struct option *opt, + const char *arg, int unset) +{ + struct hashmap *map = opt->value; + const char *delim, *value; + size_t keylen; + + BUG_ON_OPT_NEG(unset); + + delim = strchr(arg, ':'); + if (delim) { + keylen = delim - arg; + value = delim + 1; + } else { + keylen = strlen(arg); + value = arg; + } + + if (!keylen || !*value) + return error(_("--seed-anonymized token cannot be empty")); + + anonymize_str(map, anonymize_seed, arg, keylen, (void *)value); + + return 0; +} + int cmd_fast_export(int argc, const char **argv, const char *prefix) { struct rev_info revs; @@ -1188,6 +1230,9 @@ int cmd_fast_export(int argc, const char **argv, const char *prefix) OPT_STRING_LIST(0, "refspec", &refspecs_list, N_("refspec"), N_("Apply refspec to exported refs")), OPT_BOOL(0, "anonymize", &anonymize, N_("anonymize output")), + OPT_CALLBACK_F(0, "seed-anonymized", &anonymized_seeds, N_("from:to"), + N_("convert <from> to <to> in anonymized output"), + PARSE_OPT_NONEG, parse_opt_seed_anonymized), OPT_BOOL(0, "reference-excluded-parents", &reference_excluded_commits, N_("Reference parents which are not in fast-export stream by object id")), OPT_BOOL(0, "show-original-ids", &show_original_ids, diff --git a/t/t9351-fast-export-anonymize.sh b/t/t9351-fast-export-anonymize.sh index dc5d75cd19..d84eec9bab 100755 --- a/t/t9351-fast-export-anonymize.sh +++ b/t/t9351-fast-export-anonymize.sh @@ -6,6 +6,7 @@ test_description='basic tests for fast-export --anonymize' test_expect_success 'setup simple repo' ' test_commit base && test_commit foo && + test_commit retain-me && git checkout -b other HEAD^ && mkdir subdir && test_commit subdir/bar && @@ -18,7 +19,10 @@ test_expect_success 'setup simple repo' ' ' test_expect_success 'export anonymized stream' ' - git fast-export --anonymize --all >stream + git fast-export --anonymize --all \ + --seed-anonymized=retain-me \ + --seed-anonymized=xyzzy:custom-name \ + >stream ' # this also covers commit messages @@ -30,6 +34,11 @@ test_expect_success 'stream omits path names' ' ! grep xyzzy stream ' +test_expect_success 'stream contains user-specified names' ' + grep retain-me stream && + grep custom-name stream +' + test_expect_success 'stream omits gitlink oids' ' # avoid relying on the whole oid to remain hash-agnostic; this is # plenty to be unique within our test case
After you anonymize a repository, it can be hard to find which commits correspond between the original and the result, and thus hard to reproduce commands that triggered bugs in the original. Let's make it possible to seed the anonymization map. This lets users either: - mark names to be retained as-is, if they don't consider them secret (in which case their original commands would just work) - map names to new values, which lets them adapt the reproduction recipe to the new names without revealing the originals The implementation is fairly straight-forward. We already store each anonymized token in a hashmap (so that the same token appearing twice is converted to the same result). We can just introduce a new "seed" hashmap which is consulted first. This does make a few more promises to the user about how we'll anonymize things (e.g., token-splitting pathnames). But it's unlikely that we'd want to change those rules, even if the actual anonymization of a single token changes. And it makes things much easier for the user, who can unblind only a directory name without having to specify each path within it. One alternative to this approach would be to anonymize as we see fit, and then dump the whole refname and pathname mappings to a file. This does work, but it's a bit awkward to use (you have to manually dig the items you care about out of the mapping). Signed-off-by: Jeff King <peff@peff.net> --- Documentation/git-fast-export.txt | 24 ++++++++++++++++ builtin/fast-export.c | 47 ++++++++++++++++++++++++++++++- t/t9351-fast-export-anonymize.sh | 11 +++++++- 3 files changed, 80 insertions(+), 2 deletions(-)