convert: add filter.<driver>.process option

Git's clean/smudge mechanism invokes an external filter process for every single blob that is affected by a filter. If Git filters a lot of blobs then the startup time of the external filter processes can become a significant part of the overall Git execution time. In a preliminary performance test this developer used a clean/smudge filter written in golang to filter 12,000 files. This process took 364s with the existing filter mechanism and 5s with the new mechanism. See details here: https://github.com/github/git-lfs/pull/1382 This patch adds the `filter.<driver>.process` string option which, if used, keeps the external filter process running and processes all blobs with the packet format (pkt-line) based protocol over standard input and standard output. The full protocol is explained in detail in `Documentation/gitattributes.txt`. A few key decisions: * The long running filter process is referred to as filter protocol version 2 because the existing single shot filter invocation is considered version 1. * Git sends a welcome message and expects a response right after the external filter process has started. This ensures that Git will not hang if a version 1 filter is incorrectly used with the filter.<driver>.process option for version 2 filters. In addition, Git can detect this kind of error and warn the user. * The status of a filter operation (e.g. "success" or "error) is set before the actual response and (if necessary!) re-set after the response. The advantage of this two step status response is that if the filter detects an error early, then the filter can communicate this and Git does not even need to create structures to read the response. * All status responses are pkt-line lists terminated with a flush packet. This allows us to send other status fields with the same protocol in the future. Helped-by: Martin-Louis Bright <mlbright@gmail.com> Reviewed-by: Jakub Narebski <jnareb@gmail.com> Signed-off-by: Lars Schneider <larsxschneider@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Lars Schneider committed Oct 16, 2016 at 16:20 UTC edcc85814c87ebd7f3b1b7d3979fac3dfb84d308
4 files changed +1082 -10
Documentation/gitattributes.txt
+154 -1
@@ -293,7 +293,15 @@ checkout, when the `smudge` command is specified, the command is
293 fed the blob object from its standard input, and its standard
294 output is used to update the worktree file. Similarly, the
295 `clean` command is used to convert the contents of worktree file
296 -upon checkin.
296 +upon checkin. By default these commands process only a single
297 +blob and terminate. If a long running `process` filter is used
298 +in place of `clean` and/or `smudge` filters, then Git can process
299 +all blobs with a single filter command invocation for the entire
300 +life of a single Git command, for example `git add --all`. If a
301 +long running `process` filter is configured then it always takes
302 +precedence over a configured single blob filter. See section
303 +below for the description of the protocol used to communicate with
304 +a `process` filter.
305
306 One use of the content filtering is to massage the content into a shape
307 that is more convenient for the platform, filesystem, and the user to use.
@@ -373,6 +381,151 @@ not exist, or may have different contents. So, smudge and clean commands
381 should not try to access the file on disk, but only act as filters on the
382 content provided to them on standard input.
383
384 +Long Running Filter Process
385 +^^^^^^^^^^^^^^^^^^^^^^^^^^^
386 +
387 +If the filter command (a string value) is defined via
388 +`filter.<driver>.process` then Git can process all blobs with a
389 +single filter invocation for the entire life of a single Git
390 +command. This is achieved by using a packet format (pkt-line,
391 +see technical/protocol-common.txt) based protocol over standard
392 +input and standard output as follows. All packets, except for the
393 +"*CONTENT" packets and the "0000" flush packet, are considered
394 +text and therefore are terminated by a LF.
395 +
396 +Git starts the filter when it encounters the first file
397 +that needs to be cleaned or smudged. After the filter started
398 +Git sends a welcome message ("git-filter-client"), a list of supported
399 +protocol version numbers, and a flush packet. Git expects to read a welcome
400 +response message ("git-filter-server"), exactly one protocol version number
401 +from the previously sent list, and a flush packet. All further
402 +communication will be based on the selected version. The remaining
403 +protocol description below documents "version=2". Please note that
404 +"version=42" in the example below does not exist and is only there
405 +to illustrate how the protocol would look like with more than one
406 +version.
407 +
408 +After the version negotiation Git sends a list of all capabilities that
409 +it supports and a flush packet. Git expects to read a list of desired
410 +capabilities, which must be a subset of the supported capabilities list,
411 +and a flush packet as response:
412 +------------------------
413 +packet: git> git-filter-client
414 +packet: git> version=2
415 +packet: git> version=42
416 +packet: git> 0000
417 +packet: git< git-filter-server
418 +packet: git< version=2
419 +packet: git< 0000
420 +packet: git> capability=clean
421 +packet: git> capability=smudge
422 +packet: git> capability=not-yet-invented
423 +packet: git> 0000
424 +packet: git< capability=clean
425 +packet: git< capability=smudge
426 +packet: git< 0000
427 +------------------------
428 +Supported filter capabilities in version 2 are "clean" and
429 +"smudge".
430 +
431 +Afterwards Git sends a list of "key=value" pairs terminated with
432 +a flush packet. The list will contain at least the filter command
433 +(based on the supported capabilities) and the pathname of the file
434 +to filter relative to the repository root. Right after the flush packet
435 +Git sends the content split in zero or more pkt-line packets and a
436 +flush packet to terminate content. Please note, that the filter
437 +must not send any response before it received the content and the
438 +final flush packet.
439 +------------------------
440 +packet: git> command=smudge
441 +packet: git> pathname=path/testfile.dat
442 +packet: git> 0000
443 +packet: git> CONTENT
444 +packet: git> 0000
445 +------------------------
446 +
447 +The filter is expected to respond with a list of "key=value" pairs
448 +terminated with a flush packet. If the filter does not experience
449 +problems then the list must contain a "success" status. Right after
450 +these packets the filter is expected to send the content in zero
451 +or more pkt-line packets and a flush packet at the end. Finally, a
452 +second list of "key=value" pairs terminated with a flush packet
453 +is expected. The filter can change the status in the second list
454 +or keep the status as is with an empty list. Please note that the
455 +empty list must be terminated with a flush packet regardless.
456 +
457 +------------------------
458 +packet: git< status=success
459 +packet: git< 0000
460 +packet: git< SMUDGED_CONTENT
461 +packet: git< 0000
462 +packet: git< 0000 # empty list, keep "status=success" unchanged!
463 +------------------------
464 +
465 +If the result content is empty then the filter is expected to respond
466 +with a "success" status and a flush packet to signal the empty content.
467 +------------------------
468 +packet: git< status=success
469 +packet: git< 0000
470 +packet: git< 0000 # empty content!
471 +packet: git< 0000 # empty list, keep "status=success" unchanged!
472 +------------------------
473 +
474 +In case the filter cannot or does not want to process the content,
475 +it is expected to respond with an "error" status.
476 +------------------------
477 +packet: git< status=error
478 +packet: git< 0000
479 +------------------------
480 +
481 +If the filter experiences an error during processing, then it can
482 +send the status "error" after the content was (partially or
483 +completely) sent.
484 +------------------------
485 +packet: git< status=success
486 +packet: git< 0000
487 +packet: git< HALF_WRITTEN_ERRONEOUS_CONTENT
488 +packet: git< 0000
489 +packet: git< status=error
490 +packet: git< 0000
491 +------------------------
492 +
493 +In case the filter cannot or does not want to process the content
494 +as well as any future content for the lifetime of the Git process,
495 +then it is expected to respond with an "abort" status at any point
496 +in the protocol.
497 +------------------------
498 +packet: git< status=abort
499 +packet: git< 0000
500 +------------------------
501 +
502 +Git neither stops nor restarts the filter process in case the
503 +"error"/"abort" status is set. However, Git sets its exit code
504 +according to the `filter.<driver>.required` flag, mimicking the
505 +behavior of the `filter.<driver>.clean` / `filter.<driver>.smudge`
506 +mechanism.
507 +
508 +If the filter dies during the communication or does not adhere to
509 +the protocol then Git will stop the filter process and restart it
510 +with the next file that needs to be processed. Depending on the
511 +`filter.<driver>.required` flag Git will interpret that as error.
512 +
513 +After the filter has processed a blob it is expected to wait for
514 +the next "key=value" list containing a command. Git will close
515 +the command pipe on exit. The filter is expected to detect EOF
516 +and exit gracefully on its own. Git will wait until the filter
517 +process has stopped.
518 +
519 +If you develop your own long running filter
520 +process then the `GIT_TRACE_PACKET` environment variables can be
521 +very helpful for debugging (see linkgit:git[1]).
522 +
523 +Please note that you cannot use an existing `filter.<driver>.clean`
524 +or `filter.<driver>.smudge` command with `filter.<driver>.process`
525 +because the former two use a different inter process communication
526 +protocol than the latter one.
527 +
528 +
529 Interaction between checkin/checkout attributes
530 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
531
convert.c
+295 -6
@@ -3,6 +3,7 @@
3 #include "run-command.h"
4 #include "quote.h"
5 #include "sigchain.h"
6 +#include "pkt-line.h"
7
8 /*
9 * convert.c - convert a file when checking it out and checking it in.
@@ -490,11 +491,293 @@ static int apply_single_file_filter(const char *path, const char *src, size_t le
491 #define CAP_CLEAN (1u<<0)
492 #define CAP_SMUDGE (1u<<1)
493
494 +struct cmd2process {
495 + struct hashmap_entry ent; /* must be the first member! */
496 + unsigned int supported_capabilities;
497 + const char *cmd;
498 + struct child_process process;
499 +};
500 +
501 +static int cmd_process_map_initialized;
502 +static struct hashmap cmd_process_map;
503 +
504 +static int cmd2process_cmp(const struct cmd2process *e1,
505 + const struct cmd2process *e2,
506 + const void *unused)
507 +{
508 + return strcmp(e1->cmd, e2->cmd);
509 +}
510 +
511 +static struct cmd2process *find_multi_file_filter_entry(struct hashmap *hashmap, const char *cmd)
512 +{
513 + struct cmd2process key;
514 + hashmap_entry_init(&key, strhash(cmd));
515 + key.cmd = cmd;
516 + return hashmap_get(hashmap, &key, NULL);
517 +}
518 +
519 +static int packet_write_list(int fd, const char *line, ...)
520 +{
521 + va_list args;
522 + int err;
523 + va_start(args, line);
524 + for (;;) {
525 + if (!line)
526 + break;
527 + if (strlen(line) > LARGE_PACKET_DATA_MAX)
528 + return -1;
529 + err = packet_write_fmt_gently(fd, "%s\n", line);
530 + if (err)
531 + return err;
532 + line = va_arg(args, const char*);
533 + }
534 + va_end(args);
535 + return packet_flush_gently(fd);
536 +}
537 +
538 +static void read_multi_file_filter_status(int fd, struct strbuf *status)
539 +{
540 + struct strbuf **pair;
541 + char *line;
542 + for (;;) {
543 + line = packet_read_line(fd, NULL);
544 + if (!line)
545 + break;
546 + pair = strbuf_split_str(line, '=', 2);
547 + if (pair[0] && pair[0]->len && pair[1]) {
548 + /* the last "status=<foo>" line wins */
549 + if (!strcmp(pair[0]->buf, "status=")) {
550 + strbuf_reset(status);
551 + strbuf_addbuf(status, pair[1]);
552 + }
553 + }
554 + strbuf_list_free(pair);
555 + }
556 +}
557 +
558 +static void kill_multi_file_filter(struct hashmap *hashmap, struct cmd2process *entry)
559 +{
560 + if (!entry)
561 + return;
562 +
563 + entry->process.clean_on_exit = 0;
564 + kill(entry->process.pid, SIGTERM);
565 + finish_command(&entry->process);
566 +
567 + hashmap_remove(hashmap, entry, NULL);
568 + free(entry);
569 +}
570 +
571 +static void stop_multi_file_filter(struct child_process *process)
572 +{
573 + sigchain_push(SIGPIPE, SIG_IGN);
574 + /* Closing the pipe signals the filter to initiate a shutdown. */
575 + close(process->in);
576 + close(process->out);
577 + sigchain_pop(SIGPIPE);
578 + /* Finish command will wait until the shutdown is complete. */
579 + finish_command(process);
580 +}
581 +
582 +static struct cmd2process *start_multi_file_filter(struct hashmap *hashmap, const char *cmd)
583 +{
584 + int err;
585 + struct cmd2process *entry;
586 + struct child_process *process;
587 + const char *argv[] = { cmd, NULL };
588 + struct string_list cap_list = STRING_LIST_INIT_NODUP;
589 + char *cap_buf;
590 + const char *cap_name;
591 +
592 + entry = xmalloc(sizeof(*entry));
593 + entry->cmd = cmd;
594 + entry->supported_capabilities = 0;
595 + process = &entry->process;
596 +
597 + child_process_init(process);
598 + process->argv = argv;
599 + process->use_shell = 1;
600 + process->in = -1;
601 + process->out = -1;
602 + process->clean_on_exit = 1;
603 + process->clean_on_exit_handler = stop_multi_file_filter;
604 +
605 + if (start_command(process)) {
606 + error("cannot fork to run external filter '%s'", cmd);
607 + return NULL;
608 + }
609 +
610 + hashmap_entry_init(entry, strhash(cmd));
611 +
612 + sigchain_push(SIGPIPE, SIG_IGN);
613 +
614 + err = packet_write_list(process->in, "git-filter-client", "version=2", NULL);
615 + if (err)
616 + goto done;
617 +
618 + err = strcmp(packet_read_line(process->out, NULL), "git-filter-server");
619 + if (err) {
620 + error("external filter '%s' does not support filter protocol version 2", cmd);
621 + goto done;
622 + }
623 + err = strcmp(packet_read_line(process->out, NULL), "version=2");
624 + if (err)
625 + goto done;
626 + err = packet_read_line(process->out, NULL) != NULL;
627 + if (err)
628 + goto done;
629 +
630 + err = packet_write_list(process->in, "capability=clean", "capability=smudge", NULL);
631 +
632 + for (;;) {
633 + cap_buf = packet_read_line(process->out, NULL);
634 + if (!cap_buf)
635 + break;
636 + string_list_split_in_place(&cap_list, cap_buf, '=', 1);
637 +
638 + if (cap_list.nr != 2 || strcmp(cap_list.items[0].string, "capability"))
639 + continue;
640 +
641 + cap_name = cap_list.items[1].string;
642 + if (!strcmp(cap_name, "clean")) {
643 + entry->supported_capabilities |= CAP_CLEAN;
644 + } else if (!strcmp(cap_name, "smudge")) {
645 + entry->supported_capabilities |= CAP_SMUDGE;
646 + } else {
647 + warning(
648 + "external filter '%s' requested unsupported filter capability '%s'",
649 + cmd, cap_name
650 + );
651 + }
652 +
653 + string_list_clear(&cap_list, 0);
654 + }
655 +
656 +done:
657 + sigchain_pop(SIGPIPE);
658 +
659 + if (err || errno == EPIPE) {
660 + error("initialization for external filter '%s' failed", cmd);
661 + kill_multi_file_filter(hashmap, entry);
662 + return NULL;
663 + }
664 +
665 + hashmap_add(hashmap, entry);
666 + return entry;
667 +}
668 +
669 +static int apply_multi_file_filter(const char *path, const char *src, size_t len,
670 + int fd, struct strbuf *dst, const char *cmd,
671 + const unsigned int wanted_capability)
672 +{
673 + int err;
674 + struct cmd2process *entry;
675 + struct child_process *process;
676 + struct strbuf nbuf = STRBUF_INIT;
677 + struct strbuf filter_status = STRBUF_INIT;
678 + const char *filter_type;
679 +
680 + if (!cmd_process_map_initialized) {
681 + cmd_process_map_initialized = 1;
682 + hashmap_init(&cmd_process_map, (hashmap_cmp_fn) cmd2process_cmp, 0);
683 + entry = NULL;
684 + } else {
685 + entry = find_multi_file_filter_entry(&cmd_process_map, cmd);
686 + }
687 +
688 + fflush(NULL);
689 +
690 + if (!entry) {
691 + entry = start_multi_file_filter(&cmd_process_map, cmd);
692 + if (!entry)
693 + return 0;
694 + }
695 + process = &entry->process;
696 +
697 + if (!(wanted_capability & entry->supported_capabilities))
698 + return 0;
699 +
700 + if (CAP_CLEAN & wanted_capability)
701 + filter_type = "clean";
702 + else if (CAP_SMUDGE & wanted_capability)
703 + filter_type = "smudge";
704 + else
705 + die("unexpected filter type");
706 +
707 + sigchain_push(SIGPIPE, SIG_IGN);
708 +
709 + assert(strlen(filter_type) < LARGE_PACKET_DATA_MAX - strlen("command=\n"));
710 + err = packet_write_fmt_gently(process->in, "command=%s\n", filter_type);
711 + if (err)
712 + goto done;
713 +
714 + err = strlen(path) > LARGE_PACKET_DATA_MAX - strlen("pathname=\n");
715 + if (err) {
716 + error("path name too long for external filter");
717 + goto done;
718 + }
719 +
720 + err = packet_write_fmt_gently(process->in, "pathname=%s\n", path);
721 + if (err)
722 + goto done;
723 +
724 + err = packet_flush_gently(process->in);
725 + if (err)
726 + goto done;
727 +
728 + if (fd >= 0)
729 + err = write_packetized_from_fd(fd, process->in);
730 + else
731 + err = write_packetized_from_buf(src, len, process->in);
732 + if (err)
733 + goto done;
734 +
735 + read_multi_file_filter_status(process->out, &filter_status);
736 + err = strcmp(filter_status.buf, "success");
737 + if (err)
738 + goto done;
739 +
740 + err = read_packetized_to_strbuf(process->out, &nbuf) < 0;
741 + if (err)
742 + goto done;
743 +
744 + read_multi_file_filter_status(process->out, &filter_status);
745 + err = strcmp(filter_status.buf, "success");
746 +
747 +done:
748 + sigchain_pop(SIGPIPE);
749 +
750 + if (err || errno == EPIPE) {
751 + if (!strcmp(filter_status.buf, "error")) {
752 + /* The filter signaled a problem with the file. */
753 + } else if (!strcmp(filter_status.buf, "abort")) {
754 + /*
755 + * The filter signaled a permanent problem. Don't try to filter
756 + * files with the same command for the lifetime of the current
757 + * Git process.
758 + */
759 + entry->supported_capabilities &= ~wanted_capability;
760 + } else {
761 + /*
762 + * Something went wrong with the protocol filter.
763 + * Force shutdown and restart if another blob requires filtering.
764 + */
765 + error("external filter '%s' failed", cmd);
766 + kill_multi_file_filter(&cmd_process_map, entry);
767 + }
768 + } else {
769 + strbuf_swap(dst, &nbuf);
770 + }
771 + strbuf_release(&nbuf);
772 + return !err;
773 +}
774 +
775 static struct convert_driver {
776 const char *name;
777 struct convert_driver *next;
778 const char *smudge;
779 const char *clean;
780 + const char *process;
781 int required;
782 } *user_convert, **user_convert_tail;
783
@@ -510,13 +793,15 @@ static int apply_filter(const char *path, const char *src, size_t len,
793 if (!dst)
794 return 1;
795
513 - if ((CAP_CLEAN & wanted_capability) && drv->clean)
796 + if ((CAP_CLEAN & wanted_capability) && !drv->process && drv->clean)
797 cmd = drv->clean;
515 - else if ((CAP_SMUDGE & wanted_capability) && drv->smudge)
798 + else if ((CAP_SMUDGE & wanted_capability) && !drv->process && drv->smudge)
799 cmd = drv->smudge;
800
801 if (cmd && *cmd)
802 return apply_single_file_filter(path, src, len, fd, dst, cmd);
803 + else if (drv->process && *drv->process)
804 + return apply_multi_file_filter(path, src, len, fd, dst, drv->process, wanted_capability);
805
806 return 0;
807 }
@@ -558,6 +843,9 @@ static int read_convert_config(const char *var, const char *value, void *cb)
843 if (!strcmp("clean", key))
844 return git_config_string(&drv->clean, var, value);
845
846 + if (!strcmp("process", key))
847 + return git_config_string(&drv->process, var, value);
848 +
849 if (!strcmp("required", key)) {
850 drv->required = git_config_bool(var, value);
851 return 0;
@@ -919,7 +1207,7 @@ void convert_to_git_filter_fd(const char *path, int fd, struct strbuf *dst,
1207 convert_attrs(&ca, path);
1208
1209 assert(ca.drv);
922 - assert(ca.drv->clean);
1210 + assert(ca.drv->clean || ca.drv->process);
1211
1212 if (!apply_filter(path, NULL, 0, fd, dst, ca.drv, CAP_CLEAN))
1213 die("%s: clean filter '%s' failed", path, ca.drv->name);
@@ -944,9 +1232,10 @@ static int convert_to_working_tree_internal(const char *path, const char *src,
1232 }
1233 /*
1234 * CRLF conversion can be skipped if normalizing, unless there
947 - * is a smudge filter. The filter might expect CRLFs.
1235 + * is a smudge or process filter (even if the process filter doesn't
1236 + * support smudge). The filters might expect CRLFs.
1237 */
949 - if ((ca.drv && ca.drv->smudge) || !normalizing) {
1238 + if ((ca.drv && (ca.drv->smudge || ca.drv->process)) || !normalizing) {
1239 ret |= crlf_to_worktree(path, src, len, dst, ca.crlf_action);
1240 if (ret) {
1241 src = dst->buf;
@@ -1407,7 +1696,7 @@ struct stream_filter *get_stream_filter(const char *path, const unsigned char *s
1696 struct stream_filter *filter = NULL;
1697
1698 convert_attrs(&ca, path);
1410 - if (ca.drv && (ca.drv->smudge || ca.drv->clean))
1699 + if (ca.drv && (ca.drv->process || ca.drv->smudge || ca.drv->clean))
1700 return NULL;
1701
1702 if (ca.crlf_action == CRLF_AUTO || ca.crlf_action == CRLF_AUTO_CRLF)
t/t0021-conversion.sh
+441 -3
@@ -4,13 +4,72 @@ test_description='blob conversion via gitattributes'
4
5 . ./test-lib.sh
6
7 -cat <<EOF >rot13.sh
7 +TEST_ROOT="$(pwd)"
8 +
9 +cat <<EOF >"$TEST_ROOT/rot13.sh"
10 #!$SHELL_PATH
11 tr \
12 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' \
13 'nopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM'
14 EOF
13 -chmod +x rot13.sh
15 +chmod +x "$TEST_ROOT/rot13.sh"
16 +
17 +generate_random_characters () {
18 + LEN=$1
19 + NAME=$2
20 + test-genrandom some-seed $LEN |
21 + perl -pe "s/./chr((ord($&) % 26) + ord('a'))/sge" >"$TEST_ROOT/$NAME"
22 +}
23 +
24 +file_size () {
25 + cat "$1" | wc -c | sed "s/^[ ]*//"
26 +}
27 +
28 +filter_git () {
29 + rm -f rot13-filter.log &&
30 + git "$@" 2>git-stderr.log &&
31 + rm -f git-stderr.log
32 +}
33 +
34 +# Compare two files and ensure that `clean` and `smudge` respectively are
35 +# called at least once if specified in the `expect` file. The actual
36 +# invocation count is not relevant because their number can vary.
37 +# c.f. http://public-inbox.org/git/xmqqshv18i8i.fsf@gitster.mtv.corp.google.com/
38 +test_cmp_count () {
39 + expect=$1
40 + actual=$2
41 + for FILE in "$expect" "$actual"
42 + do
43 + sort "$FILE" | uniq -c | sed "s/^[ ]*//" |
44 + sed "s/^\([0-9]\) IN: clean/x IN: clean/" |
45 + sed "s/^\([0-9]\) IN: smudge/x IN: smudge/" >"$FILE.tmp" &&
46 + mv "$FILE.tmp" "$FILE"
47 + done &&
48 + test_cmp "$expect" "$actual"
49 +}
50 +
51 +# Compare two files but exclude all `clean` invocations because Git can
52 +# call `clean` zero or more times.
53 +# c.f. http://public-inbox.org/git/xmqqshv18i8i.fsf@gitster.mtv.corp.google.com/
54 +test_cmp_exclude_clean () {
55 + expect=$1
56 + actual=$2
57 + for FILE in "$expect" "$actual"
58 + do
59 + grep -v "IN: clean" "$FILE" >"$FILE.tmp" &&
60 + mv "$FILE.tmp" "$FILE"
61 + done &&
62 + test_cmp "$expect" "$actual"
63 +}
64 +
65 +# Check that the contents of two files are equal and that their rot13 version
66 +# is equal to the committed content.
67 +test_cmp_committed_rot13 () {
68 + test_cmp "$1" "$2" &&
69 + "$TEST_ROOT/rot13.sh" <"$1" >expected &&
70 + git cat-file blob :"$2" >actual &&
71 + test_cmp expected actual
72 +}
73
74 test_expect_success setup '
75 git config filter.rot13.smudge ./rot13.sh &&
@@ -31,7 +90,10 @@ test_expect_success setup '
90 cat test >test.i &&
91 git add test test.t test.i &&
92 rm -f test test.t test.i &&
34 - git checkout -- test test.t test.i
93 + git checkout -- test test.t test.i &&
94 +
95 + echo "content-test2" >test2.o &&
96 + echo "content-test3 - filename with special characters" >"test3 '\''sq'\'',\$x.o"
97 '
98
99 script='s/^\$Id: \([0-9a-f]*\) \$/\1/p'
@@ -279,4 +341,380 @@ test_expect_success 'diff does not reuse worktree files that need cleaning' '
341 test_line_count = 0 count
342 '
343
344 +test_expect_success PERL 'required process filter should filter data' '
345 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean smudge" &&
346 + test_config_global filter.protocol.required true &&
347 + rm -rf repo &&
348 + mkdir repo &&
349 + (
350 + cd repo &&
351 + git init &&
352 +
353 + echo "git-stderr.log" >.gitignore &&
354 + echo "*.r filter=protocol" >.gitattributes &&
355 + git add . &&
356 + git commit . -m "test commit 1" &&
357 + git branch empty-branch &&
358 +
359 + cp "$TEST_ROOT/test.o" test.r &&
360 + cp "$TEST_ROOT/test2.o" test2.r &&
361 + mkdir testsubdir &&
362 + cp "$TEST_ROOT/test3 '\''sq'\'',\$x.o" "testsubdir/test3 '\''sq'\'',\$x.r" &&
363 + >test4-empty.r &&
364 +
365 + S=$(file_size test.r) &&
366 + S2=$(file_size test2.r) &&
367 + S3=$(file_size "testsubdir/test3 '\''sq'\'',\$x.r") &&
368 +
369 + filter_git add . &&
370 + cat >expected.log <<-EOF &&
371 + START
372 + init handshake complete
373 + IN: clean test.r $S [OK] -- OUT: $S . [OK]
374 + IN: clean test2.r $S2 [OK] -- OUT: $S2 . [OK]
375 + IN: clean test4-empty.r 0 [OK] -- OUT: 0 [OK]
376 + IN: clean testsubdir/test3 '\''sq'\'',\$x.r $S3 [OK] -- OUT: $S3 . [OK]
377 + STOP
378 + EOF
379 + test_cmp_count expected.log rot13-filter.log &&
380 +
381 + filter_git commit . -m "test commit 2" &&
382 + cat >expected.log <<-EOF &&
383 + START
384 + init handshake complete
385 + IN: clean test.r $S [OK] -- OUT: $S . [OK]
386 + IN: clean test2.r $S2 [OK] -- OUT: $S2 . [OK]
387 + IN: clean test4-empty.r 0 [OK] -- OUT: 0 [OK]
388 + IN: clean testsubdir/test3 '\''sq'\'',\$x.r $S3 [OK] -- OUT: $S3 . [OK]
389 + IN: clean test.r $S [OK] -- OUT: $S . [OK]
390 + IN: clean test2.r $S2 [OK] -- OUT: $S2 . [OK]
391 + IN: clean test4-empty.r 0 [OK] -- OUT: 0 [OK]
392 + IN: clean testsubdir/test3 '\''sq'\'',\$x.r $S3 [OK] -- OUT: $S3 . [OK]
393 + STOP
394 + EOF
395 + test_cmp_count expected.log rot13-filter.log &&
396 +
397 + rm -f test2.r "testsubdir/test3 '\''sq'\'',\$x.r" &&
398 +
399 + filter_git checkout --quiet --no-progress . &&
400 + cat >expected.log <<-EOF &&
401 + START
402 + init handshake complete
403 + IN: smudge test2.r $S2 [OK] -- OUT: $S2 . [OK]
404 + IN: smudge testsubdir/test3 '\''sq'\'',\$x.r $S3 [OK] -- OUT: $S3 . [OK]
405 + STOP
406 + EOF
407 + test_cmp_exclude_clean expected.log rot13-filter.log &&
408 +
409 + filter_git checkout --quiet --no-progress empty-branch &&
410 + cat >expected.log <<-EOF &&
411 + START
412 + init handshake complete
413 + IN: clean test.r $S [OK] -- OUT: $S . [OK]
414 + STOP
415 + EOF
416 + test_cmp_exclude_clean expected.log rot13-filter.log &&
417 +
418 + filter_git checkout --quiet --no-progress master &&
419 + cat >expected.log <<-EOF &&
420 + START
421 + init handshake complete
422 + IN: smudge test.r $S [OK] -- OUT: $S . [OK]
423 + IN: smudge test2.r $S2 [OK] -- OUT: $S2 . [OK]
424 + IN: smudge test4-empty.r 0 [OK] -- OUT: 0 [OK]
425 + IN: smudge testsubdir/test3 '\''sq'\'',\$x.r $S3 [OK] -- OUT: $S3 . [OK]
426 + STOP
427 + EOF
428 + test_cmp_exclude_clean expected.log rot13-filter.log &&
429 +
430 + test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.r &&
431 + test_cmp_committed_rot13 "$TEST_ROOT/test2.o" test2.r &&
432 + test_cmp_committed_rot13 "$TEST_ROOT/test3 '\''sq'\'',\$x.o" "testsubdir/test3 '\''sq'\'',\$x.r"
433 + )
434 +'
435 +
436 +test_expect_success PERL 'required process filter takes precedence' '
437 + test_config_global filter.protocol.clean false &&
438 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean" &&
439 + test_config_global filter.protocol.required true &&
440 + rm -rf repo &&
441 + mkdir repo &&
442 + (
443 + cd repo &&
444 + git init &&
445 +
446 + echo "*.r filter=protocol" >.gitattributes &&
447 + cp "$TEST_ROOT/test.o" test.r &&
448 + S=$(file_size test.r) &&
449 +
450 + # Check that the process filter is invoked here
451 + filter_git add . &&
452 + cat >expected.log <<-EOF &&
453 + START
454 + init handshake complete
455 + IN: clean test.r $S [OK] -- OUT: $S . [OK]
456 + STOP
457 + EOF
458 + test_cmp_count expected.log rot13-filter.log
459 + )
460 +'
461 +
462 +test_expect_success PERL 'required process filter should be used only for "clean" operation only' '
463 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean" &&
464 + rm -rf repo &&
465 + mkdir repo &&
466 + (
467 + cd repo &&
468 + git init &&
469 +
470 + echo "*.r filter=protocol" >.gitattributes &&
471 + cp "$TEST_ROOT/test.o" test.r &&
472 + S=$(file_size test.r) &&
473 +
474 + filter_git add . &&
475 + cat >expected.log <<-EOF &&
476 + START
477 + init handshake complete
478 + IN: clean test.r $S [OK] -- OUT: $S . [OK]
479 + STOP
480 + EOF
481 + test_cmp_count expected.log rot13-filter.log &&
482 +
483 + rm test.r &&
484 +
485 + filter_git checkout --quiet --no-progress . &&
486 + # If the filter would be used for "smudge", too, we would see
487 + # "IN: smudge test.r 57 [OK] -- OUT: 57 . [OK]" here
488 + cat >expected.log <<-EOF &&
489 + START
490 + init handshake complete
491 + STOP
492 + EOF
493 + test_cmp_exclude_clean expected.log rot13-filter.log
494 + )
495 +'
496 +
497 +test_expect_success PERL 'required process filter should process multiple packets' '
498 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean smudge" &&
499 + test_config_global filter.protocol.required true &&
500 +
501 + rm -rf repo &&
502 + mkdir repo &&
503 + (
504 + cd repo &&
505 + git init &&
506 +
507 + # Generate data requiring 1, 2, 3 packets
508 + S=65516 && # PKTLINE_DATA_MAXLEN -> Maximal size of a packet
509 + generate_random_characters $(($S )) 1pkt_1__.file &&
510 + generate_random_characters $(($S +1)) 2pkt_1+1.file &&
511 + generate_random_characters $(($S*2-1)) 2pkt_2-1.file &&
512 + generate_random_characters $(($S*2 )) 2pkt_2__.file &&
513 + generate_random_characters $(($S*2+1)) 3pkt_2+1.file &&
514 +
515 + for FILE in "$TEST_ROOT"/*.file
516 + do
517 + cp "$FILE" . &&
518 + "$TEST_ROOT/rot13.sh" <"$FILE" >"$FILE.rot13"
519 + done &&
520 +
521 + echo "*.file filter=protocol" >.gitattributes &&
522 + filter_git add *.file .gitattributes &&
523 + cat >expected.log <<-EOF &&
524 + START
525 + init handshake complete
526 + IN: clean 1pkt_1__.file $(($S )) [OK] -- OUT: $(($S )) . [OK]
527 + IN: clean 2pkt_1+1.file $(($S +1)) [OK] -- OUT: $(($S +1)) .. [OK]
528 + IN: clean 2pkt_2-1.file $(($S*2-1)) [OK] -- OUT: $(($S*2-1)) .. [OK]
529 + IN: clean 2pkt_2__.file $(($S*2 )) [OK] -- OUT: $(($S*2 )) .. [OK]
530 + IN: clean 3pkt_2+1.file $(($S*2+1)) [OK] -- OUT: $(($S*2+1)) ... [OK]
531 + STOP
532 + EOF
533 + test_cmp_count expected.log rot13-filter.log &&
534 +
535 + rm -f *.file &&
536 +
537 + filter_git checkout --quiet --no-progress -- *.file &&
538 + cat >expected.log <<-EOF &&
539 + START
540 + init handshake complete
541 + IN: smudge 1pkt_1__.file $(($S )) [OK] -- OUT: $(($S )) . [OK]
542 + IN: smudge 2pkt_1+1.file $(($S +1)) [OK] -- OUT: $(($S +1)) .. [OK]
543 + IN: smudge 2pkt_2-1.file $(($S*2-1)) [OK] -- OUT: $(($S*2-1)) .. [OK]
544 + IN: smudge 2pkt_2__.file $(($S*2 )) [OK] -- OUT: $(($S*2 )) .. [OK]
545 + IN: smudge 3pkt_2+1.file $(($S*2+1)) [OK] -- OUT: $(($S*2+1)) ... [OK]
546 + STOP
547 + EOF
548 + test_cmp_exclude_clean expected.log rot13-filter.log &&
549 +
550 + for FILE in *.file
551 + do
552 + test_cmp_committed_rot13 "$TEST_ROOT/$FILE" $FILE
553 + done
554 + )
555 +'
556 +
557 +test_expect_success PERL 'required process filter with clean error should fail' '
558 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean smudge" &&
559 + test_config_global filter.protocol.required true &&
560 + rm -rf repo &&
561 + mkdir repo &&
562 + (
563 + cd repo &&
564 + git init &&
565 +
566 + echo "*.r filter=protocol" >.gitattributes &&
567 +
568 + cp "$TEST_ROOT/test.o" test.r &&
569 + echo "this is going to fail" >clean-write-fail.r &&
570 + echo "content-test3-subdir" >test3.r &&
571 +
572 + test_must_fail git add .
573 + )
574 +'
575 +
576 +test_expect_success PERL 'process filter should restart after unexpected write failure' '
577 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean smudge" &&
578 + rm -rf repo &&
579 + mkdir repo &&
580 + (
581 + cd repo &&
582 + git init &&
583 +
584 + echo "*.r filter=protocol" >.gitattributes &&
585 +
586 + cp "$TEST_ROOT/test.o" test.r &&
587 + cp "$TEST_ROOT/test2.o" test2.r &&
588 + echo "this is going to fail" >smudge-write-fail.o &&
589 + cp smudge-write-fail.o smudge-write-fail.r &&
590 +
591 + S=$(file_size test.r) &&
592 + S2=$(file_size test2.r) &&
593 + SF=$(file_size smudge-write-fail.r) &&
594 +
595 + git add . &&
596 + rm -f *.r &&
597 +
598 + rm -f rot13-filter.log &&
599 + git checkout --quiet --no-progress . 2>git-stderr.log &&
600 +
601 + grep "smudge write error at" git-stderr.log &&
602 + grep "error: external filter" git-stderr.log &&
603 +
604 + cat >expected.log <<-EOF &&
605 + START
606 + init handshake complete
607 + IN: smudge smudge-write-fail.r $SF [OK] -- OUT: $SF [WRITE FAIL]
608 + START
609 + init handshake complete
610 + IN: smudge test.r $S [OK] -- OUT: $S . [OK]
611 + IN: smudge test2.r $S2 [OK] -- OUT: $S2 . [OK]
612 + STOP
613 + EOF
614 + test_cmp_exclude_clean expected.log rot13-filter.log &&
615 +
616 + test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.r &&
617 + test_cmp_committed_rot13 "$TEST_ROOT/test2.o" test2.r &&
618 +
619 + # Smudge failed
620 + ! test_cmp smudge-write-fail.o smudge-write-fail.r &&
621 + "$TEST_ROOT/rot13.sh" <smudge-write-fail.o >expected &&
622 + git cat-file blob :smudge-write-fail.r >actual &&
623 + test_cmp expected actual
624 + )
625 +'
626 +
627 +test_expect_success PERL 'process filter should not be restarted if it signals an error' '
628 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean smudge" &&
629 + rm -rf repo &&
630 + mkdir repo &&
631 + (
632 + cd repo &&
633 + git init &&
634 +
635 + echo "*.r filter=protocol" >.gitattributes &&
636 +
637 + cp "$TEST_ROOT/test.o" test.r &&
638 + cp "$TEST_ROOT/test2.o" test2.r &&
639 + echo "this will cause an error" >error.o &&
640 + cp error.o error.r &&
641 +
642 + S=$(file_size test.r) &&
643 + S2=$(file_size test2.r) &&
644 + SE=$(file_size error.r) &&
645 +
646 + git add . &&
647 + rm -f *.r &&
648 +
649 + filter_git checkout --quiet --no-progress . &&
650 + cat >expected.log <<-EOF &&
651 + START
652 + init handshake complete
653 + IN: smudge error.r $SE [OK] -- OUT: 0 [ERROR]
654 + IN: smudge test.r $S [OK] -- OUT: $S . [OK]
655 + IN: smudge test2.r $S2 [OK] -- OUT: $S2 . [OK]
656 + STOP
657 + EOF
658 + test_cmp_exclude_clean expected.log rot13-filter.log &&
659 +
660 + test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.r &&
661 + test_cmp_committed_rot13 "$TEST_ROOT/test2.o" test2.r &&
662 + test_cmp error.o error.r
663 + )
664 +'
665 +
666 +test_expect_success PERL 'process filter abort stops processing of all further files' '
667 + test_config_global filter.protocol.process "$TEST_DIRECTORY/t0021/rot13-filter.pl clean smudge" &&
668 + rm -rf repo &&
669 + mkdir repo &&
670 + (
671 + cd repo &&
672 + git init &&
673 +
674 + echo "*.r filter=protocol" >.gitattributes &&
675 +
676 + cp "$TEST_ROOT/test.o" test.r &&
677 + cp "$TEST_ROOT/test2.o" test2.r &&
678 + echo "error this blob and all future blobs" >abort.o &&
679 + cp abort.o abort.r &&
680 +
681 + SA=$(file_size abort.r) &&
682 +
683 + git add . &&
684 + rm -f *.r &&
685 +
686 + # Note: This test assumes that Git filters files in alphabetical
687 + # order ("abort.r" before "test.r").
688 + filter_git checkout --quiet --no-progress . &&
689 + cat >expected.log <<-EOF &&
690 + START
691 + init handshake complete
692 + IN: smudge abort.r $SA [OK] -- OUT: 0 [ABORT]
693 + STOP
694 + EOF
695 + test_cmp_exclude_clean expected.log rot13-filter.log &&
696 +
697 + test_cmp "$TEST_ROOT/test.o" test.r &&
698 + test_cmp "$TEST_ROOT/test2.o" test2.r &&
699 + test_cmp abort.o abort.r
700 + )
701 +'
702 +
703 +test_expect_success PERL 'invalid process filter must fail (and not hang!)' '
704 + test_config_global filter.protocol.process cat &&
705 + test_config_global filter.protocol.required true &&
706 + rm -rf repo &&
707 + mkdir repo &&
708 + (
709 + cd repo &&
710 + git init &&
711 +
712 + echo "*.r filter=protocol" >.gitattributes &&
713 +
714 + cp "$TEST_ROOT/test.o" test.r &&
715 + test_must_fail git add . 2>git-stderr.log &&
716 + grep "does not support filter protocol version" git-stderr.log
717 + )
718 +'
719 +
720 test_done
t/t0021/rot13-filter.pl new
+192
@@ -0,0 +1,192 @@
1 +#!/usr/bin/perl
2 +#
3 +# Example implementation for the Git filter protocol version 2
4 +# See Documentation/gitattributes.txt, section "Filter Protocol"
5 +#
6 +# The script takes the list of supported protocol capabilities as
7 +# arguments ("clean", "smudge", etc).
8 +#
9 +# This implementation supports special test cases:
10 +# (1) If data with the pathname "clean-write-fail.r" is processed with
11 +# a "clean" operation then the write operation will die.
12 +# (2) If data with the pathname "smudge-write-fail.r" is processed with
13 +# a "smudge" operation then the write operation will die.
14 +# (3) If data with the pathname "error.r" is processed with any
15 +# operation then the filter signals that it cannot or does not want
16 +# to process the file.
17 +# (4) If data with the pathname "abort.r" is processed with any
18 +# operation then the filter signals that it cannot or does not want
19 +# to process the file and any file after that is processed with the
20 +# same command.
21 +#
22 +
23 +use strict;
24 +use warnings;
25 +
26 +my $MAX_PACKET_CONTENT_SIZE = 65516;
27 +my @capabilities = @ARGV;
28 +
29 +open my $debug, ">>", "rot13-filter.log" or die "cannot open log file: $!";
30 +
31 +sub rot13 {
32 + my $str = shift;
33 + $str =~ y/A-Za-z/N-ZA-Mn-za-m/;
34 + return $str;
35 +}
36 +
37 +sub packet_bin_read {
38 + my $buffer;
39 + my $bytes_read = read STDIN, $buffer, 4;
40 + if ( $bytes_read == 0 ) {
41 + # EOF - Git stopped talking to us!
42 + print $debug "STOP\n";
43 + exit();
44 + }
45 + elsif ( $bytes_read != 4 ) {
46 + die "invalid packet: '$buffer'";
47 + }
48 + my $pkt_size = hex($buffer);
49 + if ( $pkt_size == 0 ) {
50 + return ( 1, "" );
51 + }
52 + elsif ( $pkt_size > 4 ) {
53 + my $content_size = $pkt_size - 4;
54 + $bytes_read = read STDIN, $buffer, $content_size;
55 + if ( $bytes_read != $content_size ) {
56 + die "invalid packet ($content_size bytes expected; $bytes_read bytes read)";
57 + }
58 + return ( 0, $buffer );
59 + }
60 + else {
61 + die "invalid packet size: $pkt_size";
62 + }
63 +}
64 +
65 +sub packet_txt_read {
66 + my ( $res, $buf ) = packet_bin_read();
67 + unless ( $buf =~ s/\n$// ) {
68 + die "A non-binary line MUST be terminated by an LF.";
69 + }
70 + return ( $res, $buf );
71 +}
72 +
73 +sub packet_bin_write {
74 + my $buf = shift;
75 + print STDOUT sprintf( "%04x", length($buf) + 4 );
76 + print STDOUT $buf;
77 + STDOUT->flush();
78 +}
79 +
80 +sub packet_txt_write {
81 + packet_bin_write( $_[0] . "\n" );
82 +}
83 +
84 +sub packet_flush {
85 + print STDOUT sprintf( "%04x", 0 );
86 + STDOUT->flush();
87 +}
88 +
89 +print $debug "START\n";
90 +$debug->flush();
91 +
92 +( packet_txt_read() eq ( 0, "git-filter-client" ) ) || die "bad initialize";
93 +( packet_txt_read() eq ( 0, "version=2" ) ) || die "bad version";
94 +( packet_bin_read() eq ( 1, "" ) ) || die "bad version end";
95 +
96 +packet_txt_write("git-filter-server");
97 +packet_txt_write("version=2");
98 +packet_flush();
99 +
100 +( packet_txt_read() eq ( 0, "capability=clean" ) ) || die "bad capability";
101 +( packet_txt_read() eq ( 0, "capability=smudge" ) ) || die "bad capability";
102 +( packet_bin_read() eq ( 1, "" ) ) || die "bad capability end";
103 +
104 +foreach (@capabilities) {
105 + packet_txt_write( "capability=" . $_ );
106 +}
107 +packet_flush();
108 +print $debug "init handshake complete\n";
109 +$debug->flush();
110 +
111 +while (1) {
112 + my ($command) = packet_txt_read() =~ /^command=([^=]+)$/;
113 + print $debug "IN: $command";
114 + $debug->flush();
115 +
116 + my ($pathname) = packet_txt_read() =~ /^pathname=([^=]+)$/;
117 + print $debug " $pathname";
118 + $debug->flush();
119 +
120 + # Flush
121 + packet_bin_read();
122 +
123 + my $input = "";
124 + {
125 + binmode(STDIN);
126 + my $buffer;
127 + my $done = 0;
128 + while ( !$done ) {
129 + ( $done, $buffer ) = packet_bin_read();
130 + $input .= $buffer;
131 + }
132 + print $debug " " . length($input) . " [OK] -- ";
133 + $debug->flush();
134 + }
135 +
136 + my $output;
137 + if ( $pathname eq "error.r" or $pathname eq "abort.r" ) {
138 + $output = "";
139 + }
140 + elsif ( $command eq "clean" and grep( /^clean$/, @capabilities ) ) {
141 + $output = rot13($input);
142 + }
143 + elsif ( $command eq "smudge" and grep( /^smudge$/, @capabilities ) ) {
144 + $output = rot13($input);
145 + }
146 + else {
147 + die "bad command '$command'";
148 + }
149 +
150 + print $debug "OUT: " . length($output) . " ";
151 + $debug->flush();
152 +
153 + if ( $pathname eq "error.r" ) {
154 + print $debug "[ERROR]\n";
155 + $debug->flush();
156 + packet_txt_write("status=error");
157 + packet_flush();
158 + }
159 + elsif ( $pathname eq "abort.r" ) {
160 + print $debug "[ABORT]\n";
161 + $debug->flush();
162 + packet_txt_write("status=abort");
163 + packet_flush();
164 + }
165 + else {
166 + packet_txt_write("status=success");
167 + packet_flush();
168 +
169 + if ( $pathname eq "${command}-write-fail.r" ) {
170 + print $debug "[WRITE FAIL]\n";
171 + $debug->flush();
172 + die "${command} write error";
173 + }
174 +
175 + while ( length($output) > 0 ) {
176 + my $packet = substr( $output, 0, $MAX_PACKET_CONTENT_SIZE );
177 + packet_bin_write($packet);
178 + # dots represent the number of packets
179 + print $debug ".";
180 + if ( length($output) > $MAX_PACKET_CONTENT_SIZE ) {
181 + $output = substr( $output, $MAX_PACKET_CONTENT_SIZE );
182 + }
183 + else {
184 + $output = "";
185 + }
186 + }
187 + packet_flush();
188 + print $debug " [OK]\n";
189 + $debug->flush();
190 + packet_flush();
191 + }
192 +}