Merge branch 'jt/subprocess-handshake' into maint
Code cleanup. * jt/subprocess-handshake: sub-process: refactor handshake to common function Documentation: migrate sub-process docs to header convert: add "status=delayed" to filter process protocol convert: refactor capabilities negotiation convert: move multiple file filter error handling to separate function convert: put the flags field before the flag itself for consistent style t0021: write "OUT <size>" only on success t0021: make debug log file name configurable t0021: keep filter log files on comparisonmaint
						commit
						df2dd28316
					
				|  | @ -425,8 +425,8 @@ packet:          git< capability=clean | |||
| packet:          git< capability=smudge | ||||
| packet:          git< 0000 | ||||
| ------------------------ | ||||
| Supported filter capabilities in version 2 are "clean" and | ||||
| "smudge". | ||||
| Supported filter capabilities in version 2 are "clean", "smudge", | ||||
| and "delay". | ||||
|  | ||||
| Afterwards Git sends a list of "key=value" pairs terminated with | ||||
| a flush packet. The list will contain at least the filter command | ||||
|  | @ -512,12 +512,73 @@ the protocol then Git will stop the filter process and restart it | |||
| with the next file that needs to be processed. Depending on the | ||||
| `filter.<driver>.required` flag Git will interpret that as error. | ||||
|  | ||||
| After the filter has processed a blob it is expected to wait for | ||||
| the next "key=value" list containing a command. Git will close | ||||
| After the filter has processed a command it is expected to wait for | ||||
| a "key=value" list containing the next command. Git will close | ||||
| the command pipe on exit. The filter is expected to detect EOF | ||||
| and exit gracefully on its own. Git will wait until the filter | ||||
| process has stopped. | ||||
|  | ||||
| Delay | ||||
| ^^^^^ | ||||
|  | ||||
| If the filter supports the "delay" capability, then Git can send the | ||||
| flag "can-delay" after the filter command and pathname. This flag | ||||
| denotes that the filter can delay filtering the current blob (e.g. to | ||||
| compensate network latencies) by responding with no content but with | ||||
| the status "delayed" and a flush packet. | ||||
| ------------------------ | ||||
| packet:          git> command=smudge | ||||
| packet:          git> pathname=path/testfile.dat | ||||
| packet:          git> can-delay=1 | ||||
| packet:          git> 0000 | ||||
| packet:          git> CONTENT | ||||
| packet:          git> 0000 | ||||
| packet:          git< status=delayed | ||||
| packet:          git< 0000 | ||||
| ------------------------ | ||||
|  | ||||
| If the filter supports the "delay" capability then it must support the | ||||
| "list_available_blobs" command. If Git sends this command, then the | ||||
| filter is expected to return a list of pathnames representing blobs | ||||
| that have been delayed earlier and are now available. | ||||
| The list must be terminated with a flush packet followed | ||||
| by a "success" status that is also terminated with a flush packet. If | ||||
| no blobs for the delayed paths are available, yet, then the filter is | ||||
| expected to block the response until at least one blob becomes | ||||
| available. The filter can tell Git that it has no more delayed blobs | ||||
| by sending an empty list. As soon as the filter responds with an empty | ||||
| list, Git stops asking. All blobs that Git has not received at this | ||||
| point are considered missing and will result in an error. | ||||
|  | ||||
| ------------------------ | ||||
| packet:          git> command=list_available_blobs | ||||
| packet:          git> 0000 | ||||
| packet:          git< pathname=path/testfile.dat | ||||
| packet:          git< pathname=path/otherfile.dat | ||||
| packet:          git< 0000 | ||||
| packet:          git< status=success | ||||
| packet:          git< 0000 | ||||
| ------------------------ | ||||
|  | ||||
| After Git received the pathnames, it will request the corresponding | ||||
| blobs again. These requests contain a pathname and an empty content | ||||
| section. The filter is expected to respond with the smudged content | ||||
| in the usual way as explained above. | ||||
| ------------------------ | ||||
| packet:          git> command=smudge | ||||
| packet:          git> pathname=path/testfile.dat | ||||
| packet:          git> 0000 | ||||
| packet:          git> 0000  # empty content! | ||||
| packet:          git< status=success | ||||
| packet:          git< 0000 | ||||
| packet:          git< SMUDGED_CONTENT | ||||
| packet:          git< 0000 | ||||
| packet:          git< 0000  # empty list, keep "status=success" unchanged! | ||||
| ------------------------ | ||||
|  | ||||
| Example | ||||
| ^^^^^^^ | ||||
|  | ||||
| A long running filter demo implementation can be found in | ||||
| `contrib/long-running-filter/example.pl` located in the Git | ||||
| core repository. If you develop your own long running filter | ||||
|  |  | |||
|  | @ -1,59 +0,0 @@ | |||
| sub-process API | ||||
| =============== | ||||
|  | ||||
| The sub-process API makes it possible to run background sub-processes | ||||
| for the entire lifetime of a Git invocation. If Git needs to communicate | ||||
| with an external process multiple times, then this can reduces the process | ||||
| invocation overhead. Git and the sub-process communicate through stdin and | ||||
| stdout. | ||||
|  | ||||
| The sub-processes are kept in a hashmap by command name and looked up | ||||
| via the subprocess_find_entry function.  If an existing instance can not | ||||
| be found then a new process should be created and started.  When the | ||||
| parent git command terminates, all sub-processes are also terminated. | ||||
|  | ||||
| This API is based on the run-command API. | ||||
|  | ||||
| Data structures | ||||
| --------------- | ||||
|  | ||||
| * `struct subprocess_entry` | ||||
|  | ||||
| The sub-process structure.  Members should not be accessed directly. | ||||
|  | ||||
| Types | ||||
| ----- | ||||
|  | ||||
| 'int(*subprocess_start_fn)(struct subprocess_entry *entry)':: | ||||
|  | ||||
| 	User-supplied function to initialize the sub-process.  This is | ||||
| 	typically used to negotiate the interface version and capabilities. | ||||
|  | ||||
|  | ||||
| Functions | ||||
| --------- | ||||
|  | ||||
| `cmd2process_cmp`:: | ||||
|  | ||||
| 	Function to test two subprocess hashmap entries for equality. | ||||
|  | ||||
| `subprocess_start`:: | ||||
|  | ||||
| 	Start a subprocess and add it to the subprocess hashmap. | ||||
|  | ||||
| `subprocess_stop`:: | ||||
|  | ||||
| 	Kill a subprocess and remove it from the subprocess hashmap. | ||||
|  | ||||
| `subprocess_find_entry`:: | ||||
|  | ||||
| 	Find a subprocess in the subprocess hashmap. | ||||
|  | ||||
| `subprocess_get_child_process`:: | ||||
|  | ||||
| 	Get the underlying `struct child_process` from a subprocess. | ||||
|  | ||||
| `subprocess_read_status`:: | ||||
|  | ||||
| 	Helper function to read packets looking for the last "status=<foo>" | ||||
| 	key/value pair. | ||||
|  | @ -358,6 +358,8 @@ static int checkout_paths(const struct checkout_opts *opts, | |||
| 	state.force = 1; | ||||
| 	state.refresh_cache = 1; | ||||
| 	state.istate = &the_index; | ||||
|  | ||||
| 	enable_delayed_checkout(&state); | ||||
| 	for (pos = 0; pos < active_nr; pos++) { | ||||
| 		struct cache_entry *ce = active_cache[pos]; | ||||
| 		if (ce->ce_flags & CE_MATCHED) { | ||||
|  | @ -372,6 +374,7 @@ static int checkout_paths(const struct checkout_opts *opts, | |||
| 			pos = skip_same_name(ce, pos) - 1; | ||||
| 		} | ||||
| 	} | ||||
| 	errs |= finish_delayed_checkout(&state); | ||||
|  | ||||
| 	if (write_locked_index(&the_index, lock_file, COMMIT_LOCK)) | ||||
| 		die(_("unable to write new index file")); | ||||
|  |  | |||
							
								
								
									
										3
									
								
								cache.h
								
								
								
								
							
							
						
						
									
										3
									
								
								cache.h
								
								
								
								
							|  | @ -1500,6 +1500,7 @@ struct checkout { | |||
| 	struct index_state *istate; | ||||
| 	const char *base_dir; | ||||
| 	int base_dir_len; | ||||
| 	struct delayed_checkout *delayed_checkout; | ||||
| 	unsigned force:1, | ||||
| 		 quiet:1, | ||||
| 		 not_new:1, | ||||
|  | @ -1509,6 +1510,8 @@ struct checkout { | |||
|  | ||||
| #define TEMPORARY_FILENAME_LENGTH 25 | ||||
| extern int checkout_entry(struct cache_entry *ce, const struct checkout *state, char *topath); | ||||
| extern void enable_delayed_checkout(struct checkout *state); | ||||
| extern int finish_delayed_checkout(struct checkout *state); | ||||
|  | ||||
| struct cache_def { | ||||
| 	struct strbuf path; | ||||
|  |  | |||
							
								
								
									
										211
									
								
								convert.c
								
								
								
								
							
							
						
						
									
										211
									
								
								convert.c
								
								
								
								
							|  | @ -501,6 +501,7 @@ static int apply_single_file_filter(const char *path, const char *src, size_t le | |||
|  | ||||
| #define CAP_CLEAN    (1u<<0) | ||||
| #define CAP_SMUDGE   (1u<<1) | ||||
| #define CAP_DELAY    (1u<<2) | ||||
|  | ||||
| struct cmd2process { | ||||
| 	struct subprocess_entry subprocess; /* must be the first member! */ | ||||
|  | @ -512,69 +513,49 @@ static struct hashmap subprocess_map; | |||
|  | ||||
| static int start_multi_file_filter_fn(struct subprocess_entry *subprocess) | ||||
| { | ||||
| 	int err; | ||||
| 	static int versions[] = {2, 0}; | ||||
| 	static struct subprocess_capability capabilities[] = { | ||||
| 		{ "clean",  CAP_CLEAN  }, | ||||
| 		{ "smudge", CAP_SMUDGE }, | ||||
| 		{ "delay",  CAP_DELAY  }, | ||||
| 		{ NULL, 0 } | ||||
| 	}; | ||||
| 	struct cmd2process *entry = (struct cmd2process *)subprocess; | ||||
| 	struct string_list cap_list = STRING_LIST_INIT_NODUP; | ||||
| 	char *cap_buf; | ||||
| 	const char *cap_name; | ||||
| 	struct child_process *process = &subprocess->process; | ||||
| 	const char *cmd = subprocess->cmd; | ||||
|  | ||||
| 	sigchain_push(SIGPIPE, SIG_IGN); | ||||
|  | ||||
| 	err = packet_writel(process->in, "git-filter-client", "version=2", NULL); | ||||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	err = strcmp(packet_read_line(process->out, NULL), "git-filter-server"); | ||||
| 	if (err) { | ||||
| 		error("external filter '%s' does not support filter protocol version 2", cmd); | ||||
| 		goto done; | ||||
| 	return subprocess_handshake(subprocess, "git-filter", versions, NULL, | ||||
| 				    capabilities, | ||||
| 				    &entry->supported_capabilities); | ||||
| } | ||||
| 	err = strcmp(packet_read_line(process->out, NULL), "version=2"); | ||||
| 	if (err) | ||||
| 		goto done; | ||||
| 	err = packet_read_line(process->out, NULL) != NULL; | ||||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	err = packet_writel(process->in, "capability=clean", "capability=smudge", NULL); | ||||
|  | ||||
| 	for (;;) { | ||||
| 		cap_buf = packet_read_line(process->out, NULL); | ||||
| 		if (!cap_buf) | ||||
| 			break; | ||||
| 		string_list_split_in_place(&cap_list, cap_buf, '=', 1); | ||||
|  | ||||
| 		if (cap_list.nr != 2 || strcmp(cap_list.items[0].string, "capability")) | ||||
| 			continue; | ||||
|  | ||||
| 		cap_name = cap_list.items[1].string; | ||||
| 		if (!strcmp(cap_name, "clean")) { | ||||
| 			entry->supported_capabilities |= CAP_CLEAN; | ||||
| 		} else if (!strcmp(cap_name, "smudge")) { | ||||
| 			entry->supported_capabilities |= CAP_SMUDGE; | ||||
| static void handle_filter_error(const struct strbuf *filter_status, | ||||
| 				struct cmd2process *entry, | ||||
| 				const unsigned int wanted_capability) { | ||||
| 	if (!strcmp(filter_status->buf, "error")) | ||||
| 		; /* The filter signaled a problem with the file. */ | ||||
| 	else if (!strcmp(filter_status->buf, "abort") && wanted_capability) { | ||||
| 		/* | ||||
| 		 * The filter signaled a permanent problem. Don't try to filter | ||||
| 		 * files with the same command for the lifetime of the current | ||||
| 		 * Git process. | ||||
| 		 */ | ||||
| 		 entry->supported_capabilities &= ~wanted_capability; | ||||
| 	} else { | ||||
| 			warning( | ||||
| 				"external filter '%s' requested unsupported filter capability '%s'", | ||||
| 				cmd, cap_name | ||||
| 			); | ||||
| 		/* | ||||
| 		 * Something went wrong with the protocol filter. | ||||
| 		 * Force shutdown and restart if another blob requires filtering. | ||||
| 		 */ | ||||
| 		error("external filter '%s' failed", entry->subprocess.cmd); | ||||
| 		subprocess_stop(&subprocess_map, &entry->subprocess); | ||||
| 		free(entry); | ||||
| 	} | ||||
|  | ||||
| 		string_list_clear(&cap_list, 0); | ||||
| 	} | ||||
|  | ||||
| done: | ||||
| 	sigchain_pop(SIGPIPE); | ||||
|  | ||||
| 	return err; | ||||
| } | ||||
|  | ||||
| static int apply_multi_file_filter(const char *path, const char *src, size_t len, | ||||
| 				   int fd, struct strbuf *dst, const char *cmd, | ||||
| 				   const unsigned int wanted_capability) | ||||
| 				   const unsigned int wanted_capability, | ||||
| 				   struct delayed_checkout *dco) | ||||
| { | ||||
| 	int err; | ||||
| 	int can_delay = 0; | ||||
| 	struct cmd2process *entry; | ||||
| 	struct child_process *process; | ||||
| 	struct strbuf nbuf = STRBUF_INIT; | ||||
|  | @ -603,12 +584,12 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len | |||
| 	} | ||||
| 	process = &entry->subprocess.process; | ||||
|  | ||||
| 	if (!(wanted_capability & entry->supported_capabilities)) | ||||
| 	if (!(entry->supported_capabilities & wanted_capability)) | ||||
| 		return 0; | ||||
|  | ||||
| 	if (CAP_CLEAN & wanted_capability) | ||||
| 	if (wanted_capability & CAP_CLEAN) | ||||
| 		filter_type = "clean"; | ||||
| 	else if (CAP_SMUDGE & wanted_capability) | ||||
| 	else if (wanted_capability & CAP_SMUDGE) | ||||
| 		filter_type = "smudge"; | ||||
| 	else | ||||
| 		die("unexpected filter type"); | ||||
|  | @ -630,6 +611,14 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len | |||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	if ((entry->supported_capabilities & CAP_DELAY) && | ||||
| 	    dco && dco->state == CE_CAN_DELAY) { | ||||
| 		can_delay = 1; | ||||
| 		err = packet_write_fmt_gently(process->in, "can-delay=1\n"); | ||||
| 		if (err) | ||||
| 			goto done; | ||||
| 	} | ||||
|  | ||||
| 	err = packet_flush_gently(process->in); | ||||
| 	if (err) | ||||
| 		goto done; | ||||
|  | @ -645,6 +634,11 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len | |||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	if (can_delay && !strcmp(filter_status.buf, "delayed")) { | ||||
| 		string_list_insert(&dco->filters, cmd); | ||||
| 		string_list_insert(&dco->paths, path); | ||||
| 	} else { | ||||
| 		/* The filter got the blob and wants to send us a response. */ | ||||
| 		err = strcmp(filter_status.buf, "success"); | ||||
| 		if (err) | ||||
| 			goto done; | ||||
|  | @ -658,36 +652,69 @@ static int apply_multi_file_filter(const char *path, const char *src, size_t len | |||
| 			goto done; | ||||
|  | ||||
| 		err = strcmp(filter_status.buf, "success"); | ||||
| 	} | ||||
|  | ||||
| done: | ||||
| 	sigchain_pop(SIGPIPE); | ||||
|  | ||||
| 	if (err) { | ||||
| 		if (!strcmp(filter_status.buf, "error")) { | ||||
| 			/* The filter signaled a problem with the file. */ | ||||
| 		} else if (!strcmp(filter_status.buf, "abort")) { | ||||
| 			/* | ||||
| 			 * The filter signaled a permanent problem. Don't try to filter | ||||
| 			 * files with the same command for the lifetime of the current | ||||
| 			 * Git process. | ||||
| 			 */ | ||||
| 			 entry->supported_capabilities &= ~wanted_capability; | ||||
| 		} else { | ||||
| 			/* | ||||
| 			 * Something went wrong with the protocol filter. | ||||
| 			 * Force shutdown and restart if another blob requires filtering. | ||||
| 			 */ | ||||
| 			error("external filter '%s' failed", cmd); | ||||
| 			subprocess_stop(&subprocess_map, &entry->subprocess); | ||||
| 			free(entry); | ||||
| 		} | ||||
| 	} else { | ||||
| 	if (err) | ||||
| 		handle_filter_error(&filter_status, entry, wanted_capability); | ||||
| 	else | ||||
| 		strbuf_swap(dst, &nbuf); | ||||
| 	} | ||||
| 	strbuf_release(&nbuf); | ||||
| 	return !err; | ||||
| } | ||||
|  | ||||
|  | ||||
| int async_query_available_blobs(const char *cmd, struct string_list *available_paths) | ||||
| { | ||||
| 	int err; | ||||
| 	char *line; | ||||
| 	struct cmd2process *entry; | ||||
| 	struct child_process *process; | ||||
| 	struct strbuf filter_status = STRBUF_INIT; | ||||
|  | ||||
| 	assert(subprocess_map_initialized); | ||||
| 	entry = (struct cmd2process *)subprocess_find_entry(&subprocess_map, cmd); | ||||
| 	if (!entry) { | ||||
| 		error("external filter '%s' is not available anymore although " | ||||
| 		      "not all paths have been filtered", cmd); | ||||
| 		return 0; | ||||
| 	} | ||||
| 	process = &entry->subprocess.process; | ||||
| 	sigchain_push(SIGPIPE, SIG_IGN); | ||||
|  | ||||
| 	err = packet_write_fmt_gently( | ||||
| 		process->in, "command=list_available_blobs\n"); | ||||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	err = packet_flush_gently(process->in); | ||||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	while ((line = packet_read_line(process->out, NULL))) { | ||||
| 		const char *path; | ||||
| 		if (skip_prefix(line, "pathname=", &path)) | ||||
| 			string_list_insert(available_paths, xstrdup(path)); | ||||
| 		else | ||||
| 			; /* ignore unknown keys */ | ||||
| 	} | ||||
|  | ||||
| 	err = subprocess_read_status(process->out, &filter_status); | ||||
| 	if (err) | ||||
| 		goto done; | ||||
|  | ||||
| 	err = strcmp(filter_status.buf, "success"); | ||||
|  | ||||
| done: | ||||
| 	sigchain_pop(SIGPIPE); | ||||
|  | ||||
| 	if (err) | ||||
| 		handle_filter_error(&filter_status, entry, 0); | ||||
| 	return !err; | ||||
| } | ||||
|  | ||||
| static struct convert_driver { | ||||
| 	const char *name; | ||||
| 	struct convert_driver *next; | ||||
|  | @ -699,7 +726,8 @@ static struct convert_driver { | |||
|  | ||||
| static int apply_filter(const char *path, const char *src, size_t len, | ||||
| 			int fd, struct strbuf *dst, struct convert_driver *drv, | ||||
| 			const unsigned int wanted_capability) | ||||
| 			const unsigned int wanted_capability, | ||||
| 			struct delayed_checkout *dco) | ||||
| { | ||||
| 	const char *cmd = NULL; | ||||
|  | ||||
|  | @ -709,15 +737,16 @@ static int apply_filter(const char *path, const char *src, size_t len, | |||
| 	if (!dst) | ||||
| 		return 1; | ||||
|  | ||||
| 	if ((CAP_CLEAN & wanted_capability) && !drv->process && drv->clean) | ||||
| 	if ((wanted_capability & CAP_CLEAN) && !drv->process && drv->clean) | ||||
| 		cmd = drv->clean; | ||||
| 	else if ((CAP_SMUDGE & wanted_capability) && !drv->process && drv->smudge) | ||||
| 	else if ((wanted_capability & CAP_SMUDGE) && !drv->process && drv->smudge) | ||||
| 		cmd = drv->smudge; | ||||
|  | ||||
| 	if (cmd && *cmd) | ||||
| 		return apply_single_file_filter(path, src, len, fd, dst, cmd); | ||||
| 	else if (drv->process && *drv->process) | ||||
| 		return apply_multi_file_filter(path, src, len, fd, dst, drv->process, wanted_capability); | ||||
| 		return apply_multi_file_filter(path, src, len, fd, dst, | ||||
| 			drv->process, wanted_capability, dco); | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | @ -1058,7 +1087,7 @@ int would_convert_to_git_filter_fd(const char *path) | |||
| 	if (!ca.drv->required) | ||||
| 		return 0; | ||||
|  | ||||
| 	return apply_filter(path, NULL, 0, -1, NULL, ca.drv, CAP_CLEAN); | ||||
| 	return apply_filter(path, NULL, 0, -1, NULL, ca.drv, CAP_CLEAN, NULL); | ||||
| } | ||||
|  | ||||
| const char *get_convert_attr_ascii(const char *path) | ||||
|  | @ -1096,7 +1125,7 @@ int convert_to_git(const struct index_state *istate, | |||
|  | ||||
| 	convert_attrs(&ca, path); | ||||
|  | ||||
| 	ret |= apply_filter(path, src, len, -1, dst, ca.drv, CAP_CLEAN); | ||||
| 	ret |= apply_filter(path, src, len, -1, dst, ca.drv, CAP_CLEAN, NULL); | ||||
| 	if (!ret && ca.drv && ca.drv->required) | ||||
| 		die("%s: clean filter '%s' failed", path, ca.drv->name); | ||||
|  | ||||
|  | @ -1122,7 +1151,7 @@ void convert_to_git_filter_fd(const struct index_state *istate, | |||
| 	assert(ca.drv); | ||||
| 	assert(ca.drv->clean || ca.drv->process); | ||||
|  | ||||
| 	if (!apply_filter(path, NULL, 0, fd, dst, ca.drv, CAP_CLEAN)) | ||||
| 	if (!apply_filter(path, NULL, 0, fd, dst, ca.drv, CAP_CLEAN, NULL)) | ||||
| 		die("%s: clean filter '%s' failed", path, ca.drv->name); | ||||
|  | ||||
| 	crlf_to_git(istate, path, dst->buf, dst->len, dst, ca.crlf_action, checksafe); | ||||
|  | @ -1131,7 +1160,7 @@ void convert_to_git_filter_fd(const struct index_state *istate, | |||
|  | ||||
| static int convert_to_working_tree_internal(const char *path, const char *src, | ||||
| 					    size_t len, struct strbuf *dst, | ||||
| 					    int normalizing) | ||||
| 					    int normalizing, struct delayed_checkout *dco) | ||||
| { | ||||
| 	int ret = 0, ret_filter = 0; | ||||
| 	struct conv_attrs ca; | ||||
|  | @ -1156,22 +1185,30 @@ static int convert_to_working_tree_internal(const char *path, const char *src, | |||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ret_filter = apply_filter(path, src, len, -1, dst, ca.drv, CAP_SMUDGE); | ||||
| 	ret_filter = apply_filter( | ||||
| 		path, src, len, -1, dst, ca.drv, CAP_SMUDGE, dco); | ||||
| 	if (!ret_filter && ca.drv && ca.drv->required) | ||||
| 		die("%s: smudge filter %s failed", path, ca.drv->name); | ||||
|  | ||||
| 	return ret | ret_filter; | ||||
| } | ||||
|  | ||||
| int async_convert_to_working_tree(const char *path, const char *src, | ||||
| 				  size_t len, struct strbuf *dst, | ||||
| 				  void *dco) | ||||
| { | ||||
| 	return convert_to_working_tree_internal(path, src, len, dst, 0, dco); | ||||
| } | ||||
|  | ||||
| int convert_to_working_tree(const char *path, const char *src, size_t len, struct strbuf *dst) | ||||
| { | ||||
| 	return convert_to_working_tree_internal(path, src, len, dst, 0); | ||||
| 	return convert_to_working_tree_internal(path, src, len, dst, 0, NULL); | ||||
| } | ||||
|  | ||||
| int renormalize_buffer(const struct index_state *istate, const char *path, | ||||
| 		       const char *src, size_t len, struct strbuf *dst) | ||||
| { | ||||
| 	int ret = convert_to_working_tree_internal(path, src, len, dst, 1); | ||||
| 	int ret = convert_to_working_tree_internal(path, src, len, dst, 1, NULL); | ||||
| 	if (ret) { | ||||
| 		src = dst->buf; | ||||
| 		len = dst->len; | ||||
|  |  | |||
							
								
								
									
										26
									
								
								convert.h
								
								
								
								
							
							
						
						
									
										26
									
								
								convert.h
								
								
								
								
							|  | @ -4,6 +4,8 @@ | |||
| #ifndef CONVERT_H | ||||
| #define CONVERT_H | ||||
|  | ||||
| #include "string-list.h" | ||||
|  | ||||
| struct index_state; | ||||
|  | ||||
| enum safe_crlf { | ||||
|  | @ -34,6 +36,26 @@ enum eol { | |||
| #endif | ||||
| }; | ||||
|  | ||||
| enum ce_delay_state { | ||||
| 	CE_NO_DELAY = 0, | ||||
| 	CE_CAN_DELAY = 1, | ||||
| 	CE_RETRY = 2 | ||||
| }; | ||||
|  | ||||
| struct delayed_checkout { | ||||
| 	/* | ||||
| 	 * State of the currently processed cache entry. If the state is | ||||
| 	 * CE_CAN_DELAY, then the filter can delay the current cache entry. | ||||
| 	 * If the state is CE_RETRY, then this signals the filter that the | ||||
| 	 * cache entry was requested before. | ||||
| 	 */ | ||||
| 	enum ce_delay_state state; | ||||
| 	/* List of filter drivers that signaled delayed blobs. */ | ||||
| 	struct string_list filters; | ||||
| 	/* List of delayed blobs identified by their path. */ | ||||
| 	struct string_list paths; | ||||
| }; | ||||
|  | ||||
| extern enum eol core_eol; | ||||
| extern const char *get_cached_convert_stats_ascii(const struct index_state *istate, | ||||
| 						  const char *path); | ||||
|  | @ -46,6 +68,10 @@ extern int convert_to_git(const struct index_state *istate, | |||
| 			  struct strbuf *dst, enum safe_crlf checksafe); | ||||
| extern int convert_to_working_tree(const char *path, const char *src, | ||||
| 				   size_t len, struct strbuf *dst); | ||||
| extern int async_convert_to_working_tree(const char *path, const char *src, | ||||
| 					 size_t len, struct strbuf *dst, | ||||
| 					 void *dco); | ||||
| extern int async_query_available_blobs(const char *cmd, struct string_list *available_paths); | ||||
| extern int renormalize_buffer(const struct index_state *istate, | ||||
| 			      const char *path, const char *src, size_t len, | ||||
| 			      struct strbuf *dst); | ||||
|  |  | |||
							
								
								
									
										126
									
								
								entry.c
								
								
								
								
							
							
						
						
									
										126
									
								
								entry.c
								
								
								
								
							|  | @ -137,6 +137,105 @@ static int streaming_write_entry(const struct cache_entry *ce, char *path, | |||
| 	return result; | ||||
| } | ||||
|  | ||||
| void enable_delayed_checkout(struct checkout *state) | ||||
| { | ||||
| 	if (!state->delayed_checkout) { | ||||
| 		state->delayed_checkout = xmalloc(sizeof(*state->delayed_checkout)); | ||||
| 		state->delayed_checkout->state = CE_CAN_DELAY; | ||||
| 		string_list_init(&state->delayed_checkout->filters, 0); | ||||
| 		string_list_init(&state->delayed_checkout->paths, 0); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| static int remove_available_paths(struct string_list_item *item, void *cb_data) | ||||
| { | ||||
| 	struct string_list *available_paths = cb_data; | ||||
| 	struct string_list_item *available; | ||||
|  | ||||
| 	available = string_list_lookup(available_paths, item->string); | ||||
| 	if (available) | ||||
| 		available->util = (void *)item->string; | ||||
| 	return !available; | ||||
| } | ||||
|  | ||||
| int finish_delayed_checkout(struct checkout *state) | ||||
| { | ||||
| 	int errs = 0; | ||||
| 	struct string_list_item *filter, *path; | ||||
| 	struct delayed_checkout *dco = state->delayed_checkout; | ||||
|  | ||||
| 	if (!state->delayed_checkout) | ||||
| 		return errs; | ||||
|  | ||||
| 	dco->state = CE_RETRY; | ||||
| 	while (dco->filters.nr > 0) { | ||||
| 		for_each_string_list_item(filter, &dco->filters) { | ||||
| 			struct string_list available_paths = STRING_LIST_INIT_NODUP; | ||||
|  | ||||
| 			if (!async_query_available_blobs(filter->string, &available_paths)) { | ||||
| 				/* Filter reported an error */ | ||||
| 				errs = 1; | ||||
| 				filter->string = ""; | ||||
| 				continue; | ||||
| 			} | ||||
| 			if (available_paths.nr <= 0) { | ||||
| 				/* | ||||
| 				 * Filter responded with no entries. That means | ||||
| 				 * the filter is done and we can remove the | ||||
| 				 * filter from the list (see | ||||
| 				 * "string_list_remove_empty_items" call below). | ||||
| 				 */ | ||||
| 				filter->string = ""; | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			/* | ||||
| 			 * In dco->paths we store a list of all delayed paths. | ||||
| 			 * The filter just send us a list of available paths. | ||||
| 			 * Remove them from the list. | ||||
| 			 */ | ||||
| 			filter_string_list(&dco->paths, 0, | ||||
| 				&remove_available_paths, &available_paths); | ||||
|  | ||||
| 			for_each_string_list_item(path, &available_paths) { | ||||
| 				struct cache_entry* ce; | ||||
|  | ||||
| 				if (!path->util) { | ||||
| 					error("external filter '%s' signaled that '%s' " | ||||
| 					      "is now available although it has not been " | ||||
| 					      "delayed earlier", | ||||
| 					      filter->string, path->string); | ||||
| 					errs |= 1; | ||||
|  | ||||
| 					/* | ||||
| 					 * Do not ask the filter for available blobs, | ||||
| 					 * again, as the filter is likely buggy. | ||||
| 					 */ | ||||
| 					filter->string = ""; | ||||
| 					continue; | ||||
| 				} | ||||
| 				ce = index_file_exists(state->istate, path->string, | ||||
| 						       strlen(path->string), 0); | ||||
| 				errs |= (ce ? checkout_entry(ce, state, NULL) : 1); | ||||
| 			} | ||||
| 		} | ||||
| 		string_list_remove_empty_items(&dco->filters, 0); | ||||
| 	} | ||||
| 	string_list_clear(&dco->filters, 0); | ||||
|  | ||||
| 	/* At this point we should not have any delayed paths anymore. */ | ||||
| 	errs |= dco->paths.nr; | ||||
| 	for_each_string_list_item(path, &dco->paths) { | ||||
| 		error("'%s' was not filtered properly", path->string); | ||||
| 	} | ||||
| 	string_list_clear(&dco->paths, 0); | ||||
|  | ||||
| 	free(dco); | ||||
| 	state->delayed_checkout = NULL; | ||||
|  | ||||
| 	return errs; | ||||
| } | ||||
|  | ||||
| static int write_entry(struct cache_entry *ce, | ||||
| 		       char *path, const struct checkout *state, int to_tempfile) | ||||
| { | ||||
|  | @ -179,12 +278,35 @@ static int write_entry(struct cache_entry *ce, | |||
| 		/* | ||||
| 		 * Convert from git internal format to working tree format | ||||
| 		 */ | ||||
| 		if (ce_mode_s_ifmt == S_IFREG && | ||||
| 		    convert_to_working_tree(ce->name, new, size, &buf)) { | ||||
| 		if (ce_mode_s_ifmt == S_IFREG) { | ||||
| 			struct delayed_checkout *dco = state->delayed_checkout; | ||||
| 			if (dco && dco->state != CE_NO_DELAY) { | ||||
| 				/* Do not send the blob in case of a retry. */ | ||||
| 				if (dco->state == CE_RETRY) { | ||||
| 					new = NULL; | ||||
| 					size = 0; | ||||
| 				} | ||||
| 				ret = async_convert_to_working_tree( | ||||
| 					ce->name, new, size, &buf, dco); | ||||
| 				if (ret && string_list_has_string(&dco->paths, ce->name)) { | ||||
| 					free(new); | ||||
| 					goto finish; | ||||
| 				} | ||||
| 			} else | ||||
| 				ret = convert_to_working_tree( | ||||
| 					ce->name, new, size, &buf); | ||||
|  | ||||
| 			if (ret) { | ||||
| 				free(new); | ||||
| 				new = strbuf_detach(&buf, &newsize); | ||||
| 				size = newsize; | ||||
| 			} | ||||
| 			/* | ||||
| 			 * No "else" here as errors from convert are OK at this | ||||
| 			 * point. If the error would have been fatal (e.g. | ||||
| 			 * filter is required), then we would have died already. | ||||
| 			 */ | ||||
| 		} | ||||
|  | ||||
| 		fd = open_output_fd(path, ce, to_tempfile); | ||||
| 		if (fd < 0) { | ||||
|  |  | |||
							
								
								
									
										19
									
								
								pkt-line.c
								
								
								
								
							
							
						
						
									
										19
									
								
								pkt-line.c
								
								
								
								
							|  | @ -171,25 +171,6 @@ int packet_write_fmt_gently(int fd, const char *fmt, ...) | |||
| 	return status; | ||||
| } | ||||
|  | ||||
| int packet_writel(int fd, const char *line, ...) | ||||
| { | ||||
| 	va_list args; | ||||
| 	int err; | ||||
| 	va_start(args, line); | ||||
| 	for (;;) { | ||||
| 		if (!line) | ||||
| 			break; | ||||
| 		if (strlen(line) > LARGE_PACKET_DATA_MAX) | ||||
| 			return -1; | ||||
| 		err = packet_write_fmt_gently(fd, "%s\n", line); | ||||
| 		if (err) | ||||
| 			return err; | ||||
| 		line = va_arg(args, const char*); | ||||
| 	} | ||||
| 	va_end(args); | ||||
| 	return packet_flush_gently(fd); | ||||
| } | ||||
|  | ||||
| static int packet_write_gently(const int fd_out, const char *buf, size_t size) | ||||
| { | ||||
| 	static char packet_write_buffer[LARGE_PACKET_MAX]; | ||||
|  |  | |||
|  | @ -25,8 +25,6 @@ void packet_buf_flush(struct strbuf *buf); | |||
| void packet_buf_write(struct strbuf *buf, const char *fmt, ...) __attribute__((format (printf, 2, 3))); | ||||
| int packet_flush_gently(int fd); | ||||
| int packet_write_fmt_gently(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3))); | ||||
| LAST_ARG_MUST_BE_NULL | ||||
| int packet_writel(int fd, const char *line, ...); | ||||
| int write_packetized_from_fd(int fd_in, int fd_out); | ||||
| int write_packetized_from_buf(const char *src_in, size_t len, int fd_out); | ||||
|  | ||||
|  |  | |||
							
								
								
									
										104
									
								
								sub-process.c
								
								
								
								
							
							
						
						
									
										104
									
								
								sub-process.c
								
								
								
								
							|  | @ -105,3 +105,107 @@ int subprocess_start(struct hashmap *hashmap, struct subprocess_entry *entry, co | |||
| 	hashmap_add(hashmap, entry); | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| static int handshake_version(struct child_process *process, | ||||
| 			     const char *welcome_prefix, int *versions, | ||||
| 			     int *chosen_version) | ||||
| { | ||||
| 	int version_scratch; | ||||
| 	int i; | ||||
| 	char *line; | ||||
| 	const char *p; | ||||
|  | ||||
| 	if (!chosen_version) | ||||
| 		chosen_version = &version_scratch; | ||||
|  | ||||
| 	if (packet_write_fmt_gently(process->in, "%s-client\n", | ||||
| 				    welcome_prefix)) | ||||
| 		return error("Could not write client identification"); | ||||
| 	for (i = 0; versions[i]; i++) { | ||||
| 		if (packet_write_fmt_gently(process->in, "version=%d\n", | ||||
| 					    versions[i])) | ||||
| 			return error("Could not write requested version"); | ||||
| 	} | ||||
| 	if (packet_flush_gently(process->in)) | ||||
| 		return error("Could not write flush packet"); | ||||
|  | ||||
| 	if (!(line = packet_read_line(process->out, NULL)) || | ||||
| 	    !skip_prefix(line, welcome_prefix, &p) || | ||||
| 	    strcmp(p, "-server")) | ||||
| 		return error("Unexpected line '%s', expected %s-server", | ||||
| 			     line ? line : "<flush packet>", welcome_prefix); | ||||
| 	if (!(line = packet_read_line(process->out, NULL)) || | ||||
| 	    !skip_prefix(line, "version=", &p) || | ||||
| 	    strtol_i(p, 10, chosen_version)) | ||||
| 		return error("Unexpected line '%s', expected version", | ||||
| 			     line ? line : "<flush packet>"); | ||||
| 	if ((line = packet_read_line(process->out, NULL))) | ||||
| 		return error("Unexpected line '%s', expected flush", line); | ||||
|  | ||||
| 	/* Check to make sure that the version received is supported */ | ||||
| 	for (i = 0; versions[i]; i++) { | ||||
| 		if (versions[i] == *chosen_version) | ||||
| 			break; | ||||
| 	} | ||||
| 	if (!versions[i]) | ||||
| 		return error("Version %d not supported", *chosen_version); | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| static int handshake_capabilities(struct child_process *process, | ||||
| 				  struct subprocess_capability *capabilities, | ||||
| 				  unsigned int *supported_capabilities) | ||||
| { | ||||
| 	int i; | ||||
| 	char *line; | ||||
|  | ||||
| 	for (i = 0; capabilities[i].name; i++) { | ||||
| 		if (packet_write_fmt_gently(process->in, "capability=%s\n", | ||||
| 					    capabilities[i].name)) | ||||
| 			return error("Could not write requested capability"); | ||||
| 	} | ||||
| 	if (packet_flush_gently(process->in)) | ||||
| 		return error("Could not write flush packet"); | ||||
|  | ||||
| 	while ((line = packet_read_line(process->out, NULL))) { | ||||
| 		const char *p; | ||||
| 		if (!skip_prefix(line, "capability=", &p)) | ||||
| 			continue; | ||||
|  | ||||
| 		for (i = 0; | ||||
| 		     capabilities[i].name && strcmp(p, capabilities[i].name); | ||||
| 		     i++) | ||||
| 			; | ||||
| 		if (capabilities[i].name) { | ||||
| 			if (supported_capabilities) | ||||
| 				*supported_capabilities |= capabilities[i].flag; | ||||
| 		} else { | ||||
| 			warning("external filter requested unsupported filter capability '%s'", | ||||
| 				p); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| int subprocess_handshake(struct subprocess_entry *entry, | ||||
| 			 const char *welcome_prefix, | ||||
| 			 int *versions, | ||||
| 			 int *chosen_version, | ||||
| 			 struct subprocess_capability *capabilities, | ||||
| 			 unsigned int *supported_capabilities) | ||||
| { | ||||
| 	int retval; | ||||
| 	struct child_process *process = &entry->process; | ||||
|  | ||||
| 	sigchain_push(SIGPIPE, SIG_IGN); | ||||
|  | ||||
| 	retval = handshake_version(process, welcome_prefix, versions, | ||||
| 				   chosen_version) || | ||||
| 		 handshake_capabilities(process, capabilities, | ||||
| 					supported_capabilities); | ||||
|  | ||||
| 	sigchain_pop(SIGPIPE); | ||||
| 	return retval; | ||||
| } | ||||
|  |  | |||
|  | @ -6,41 +6,88 @@ | |||
| #include "run-command.h" | ||||
|  | ||||
| /* | ||||
|  * Generic implementation of background process infrastructure. | ||||
|  * See: Documentation/technical/api-sub-process.txt | ||||
|  * The sub-process API makes it possible to run background sub-processes | ||||
|  * for the entire lifetime of a Git invocation. If Git needs to communicate | ||||
|  * with an external process multiple times, then this can reduces the process | ||||
|  * invocation overhead. Git and the sub-process communicate through stdin and | ||||
|  * stdout. | ||||
|  * | ||||
|  * The sub-processes are kept in a hashmap by command name and looked up | ||||
|  * via the subprocess_find_entry function.  If an existing instance can not | ||||
|  * be found then a new process should be created and started.  When the | ||||
|  * parent git command terminates, all sub-processes are also terminated. | ||||
|  * | ||||
|  * This API is based on the run-command API. | ||||
|  */ | ||||
|  | ||||
|  /* data structures */ | ||||
|  | ||||
| /* Members should not be accessed directly. */ | ||||
| struct subprocess_entry { | ||||
| 	struct hashmap_entry ent; /* must be the first member! */ | ||||
| 	const char *cmd; | ||||
| 	struct child_process process; | ||||
| }; | ||||
|  | ||||
| struct subprocess_capability { | ||||
| 	const char *name; | ||||
|  | ||||
| 	/* | ||||
| 	 * subprocess_handshake will "|=" this value to supported_capabilities | ||||
| 	 * if the server reports that it supports this capability. | ||||
| 	 */ | ||||
| 	unsigned int flag; | ||||
| }; | ||||
|  | ||||
| /* subprocess functions */ | ||||
|  | ||||
| /* Function to test two subprocess hashmap entries for equality. */ | ||||
| extern int cmd2process_cmp(const void *unused_cmp_data, | ||||
| 			   const struct subprocess_entry *e1, | ||||
| 			   const struct subprocess_entry *e2, | ||||
| 			   const void *unused_keydata); | ||||
|  | ||||
| /* | ||||
|  * User-supplied function to initialize the sub-process.  This is | ||||
|  * typically used to negotiate the interface version and capabilities. | ||||
|  */ | ||||
| typedef int(*subprocess_start_fn)(struct subprocess_entry *entry); | ||||
|  | ||||
| /* Start a subprocess and add it to the subprocess hashmap. */ | ||||
| int subprocess_start(struct hashmap *hashmap, struct subprocess_entry *entry, const char *cmd, | ||||
| 		subprocess_start_fn startfn); | ||||
|  | ||||
| /* Kill a subprocess and remove it from the subprocess hashmap. */ | ||||
| void subprocess_stop(struct hashmap *hashmap, struct subprocess_entry *entry); | ||||
|  | ||||
| /* Find a subprocess in the subprocess hashmap. */ | ||||
| struct subprocess_entry *subprocess_find_entry(struct hashmap *hashmap, const char *cmd); | ||||
|  | ||||
| /* subprocess helper functions */ | ||||
|  | ||||
| /* Get the underlying `struct child_process` from a subprocess. */ | ||||
| static inline struct child_process *subprocess_get_child_process( | ||||
| 		struct subprocess_entry *entry) | ||||
| { | ||||
| 	return &entry->process; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Perform the version and capability negotiation as described in the "Long | ||||
|  * Running Filter Process" section of the gitattributes documentation using the | ||||
|  * given requested versions and capabilities. The "versions" and "capabilities" | ||||
|  * parameters are arrays terminated by a 0 or blank struct. | ||||
|  * | ||||
|  * This function is typically called when a subprocess is started (as part of | ||||
|  * the "startfn" passed to subprocess_start). | ||||
|  */ | ||||
| int subprocess_handshake(struct subprocess_entry *entry, | ||||
| 			 const char *welcome_prefix, | ||||
| 			 int *versions, | ||||
| 			 int *chosen_version, | ||||
| 			 struct subprocess_capability *capabilities, | ||||
| 			 unsigned int *supported_capabilities); | ||||
|  | ||||
| /* | ||||
|  * Helper function that will read packets looking for "status=<foo>" | ||||
|  * key/value pairs and return the value from the last "status" packet | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ file_size () { | |||
| } | ||||
|  | ||||
| filter_git () { | ||||
| 	rm -f rot13-filter.log && | ||||
| 	rm -f *.log && | ||||
| 	git "$@" | ||||
| } | ||||
|  | ||||
|  | @ -42,10 +42,10 @@ test_cmp_count () { | |||
| 	for FILE in "$expect" "$actual" | ||||
| 	do | ||||
| 		sort "$FILE" | uniq -c | | ||||
| 		sed -e "s/^ *[0-9][0-9]*[ 	]*IN: /x IN: /" >"$FILE.tmp" && | ||||
| 		mv "$FILE.tmp" "$FILE" || return | ||||
| 		sed -e "s/^ *[0-9][0-9]*[ 	]*IN: /x IN: /" >"$FILE.tmp" | ||||
| 	done && | ||||
| 	test_cmp "$expect" "$actual" | ||||
| 	test_cmp "$expect.tmp" "$actual.tmp" && | ||||
| 	rm "$expect.tmp" "$actual.tmp" | ||||
| } | ||||
|  | ||||
| # Compare two files but exclude all `clean` invocations because Git can | ||||
|  | @ -56,10 +56,10 @@ test_cmp_exclude_clean () { | |||
| 	actual=$2 | ||||
| 	for FILE in "$expect" "$actual" | ||||
| 	do | ||||
| 		grep -v "IN: clean" "$FILE" >"$FILE.tmp" && | ||||
| 		mv "$FILE.tmp" "$FILE" | ||||
| 		grep -v "IN: clean" "$FILE" >"$FILE.tmp" | ||||
| 	done && | ||||
| 	test_cmp "$expect" "$actual" | ||||
| 	test_cmp "$expect.tmp" "$actual.tmp" && | ||||
| 	rm "$expect.tmp" "$actual.tmp" | ||||
| } | ||||
|  | ||||
| # Check that the contents of two files are equal and that their rot13 version | ||||
|  | @ -342,7 +342,7 @@ test_expect_success 'diff does not reuse worktree files that need cleaning' ' | |||
| ' | ||||
|  | ||||
| test_expect_success PERL 'required process filter should filter data' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean smudge" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean smudge" && | ||||
| 	test_config_global filter.protocol.required true && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
|  | @ -375,7 +375,7 @@ test_expect_success PERL 'required process filter should filter data' ' | |||
| 			IN: clean testsubdir/test3 '\''sq'\'',\$x=.r $S3 [OK] -- OUT: $S3 . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_count expected.log rot13-filter.log && | ||||
| 		test_cmp_count expected.log debug.log && | ||||
|  | ||||
| 		git commit -m "test commit 2" && | ||||
| 		rm -f test2.r "testsubdir/test3 '\''sq'\'',\$x=.r" && | ||||
|  | @ -388,7 +388,7 @@ test_expect_success PERL 'required process filter should filter data' ' | |||
| 			IN: smudge testsubdir/test3 '\''sq'\'',\$x=.r $S3 [OK] -- OUT: $S3 . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		filter_git checkout --quiet --no-progress empty-branch && | ||||
| 		cat >expected.log <<-EOF && | ||||
|  | @ -397,7 +397,7 @@ test_expect_success PERL 'required process filter should filter data' ' | |||
| 			IN: clean test.r $S [OK] -- OUT: $S . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		filter_git checkout --quiet --no-progress master && | ||||
| 		cat >expected.log <<-EOF && | ||||
|  | @ -409,7 +409,7 @@ test_expect_success PERL 'required process filter should filter data' ' | |||
| 			IN: smudge testsubdir/test3 '\''sq'\'',\$x=.r $S3 [OK] -- OUT: $S3 . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.r && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test2.o" test2.r && | ||||
|  | @ -419,7 +419,7 @@ test_expect_success PERL 'required process filter should filter data' ' | |||
|  | ||||
| test_expect_success PERL 'required process filter takes precedence' ' | ||||
| 	test_config_global filter.protocol.clean false && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean" && | ||||
| 	test_config_global filter.protocol.required true && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
|  | @ -439,12 +439,12 @@ test_expect_success PERL 'required process filter takes precedence' ' | |||
| 			IN: clean test.r $S [OK] -- OUT: $S . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_count expected.log rot13-filter.log | ||||
| 		test_cmp_count expected.log debug.log | ||||
| 	) | ||||
| ' | ||||
|  | ||||
| test_expect_success PERL 'required process filter should be used only for "clean" operation only' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean" && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
|  | @ -462,7 +462,7 @@ test_expect_success PERL 'required process filter should be used only for "clean | |||
| 			IN: clean test.r $S [OK] -- OUT: $S . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_count expected.log rot13-filter.log && | ||||
| 		test_cmp_count expected.log debug.log && | ||||
|  | ||||
| 		rm test.r && | ||||
|  | ||||
|  | @ -474,12 +474,12 @@ test_expect_success PERL 'required process filter should be used only for "clean | |||
| 			init handshake complete | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log | ||||
| 		test_cmp_exclude_clean expected.log debug.log | ||||
| 	) | ||||
| ' | ||||
|  | ||||
| test_expect_success PERL 'required process filter should process multiple packets' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean smudge" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean smudge" && | ||||
| 	test_config_global filter.protocol.required true && | ||||
|  | ||||
| 	rm -rf repo && | ||||
|  | @ -514,7 +514,7 @@ test_expect_success PERL 'required process filter should process multiple packet | |||
| 			IN: clean 3pkt_2+1.file $(($S*2+1)) [OK] -- OUT: $(($S*2+1)) ... [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_count expected.log rot13-filter.log && | ||||
| 		test_cmp_count expected.log debug.log && | ||||
|  | ||||
| 		rm -f *.file && | ||||
|  | ||||
|  | @ -529,7 +529,7 @@ test_expect_success PERL 'required process filter should process multiple packet | |||
| 			IN: smudge 3pkt_2+1.file $(($S*2+1)) [OK] -- OUT: $(($S*2+1)) ... [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		for FILE in *.file | ||||
| 		do | ||||
|  | @ -539,7 +539,7 @@ test_expect_success PERL 'required process filter should process multiple packet | |||
| ' | ||||
|  | ||||
| test_expect_success PERL 'required process filter with clean error should fail' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean smudge" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean smudge" && | ||||
| 	test_config_global filter.protocol.required true && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
|  | @ -558,7 +558,7 @@ test_expect_success PERL 'required process filter with clean error should fail' | |||
| ' | ||||
|  | ||||
| test_expect_success PERL 'process filter should restart after unexpected write failure' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean smudge" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean smudge" && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
|  | @ -579,7 +579,7 @@ test_expect_success PERL 'process filter should restart after unexpected write f | |||
| 		git add . && | ||||
| 		rm -f *.r && | ||||
|  | ||||
| 		rm -f rot13-filter.log && | ||||
| 		rm -f debug.log && | ||||
| 		git checkout --quiet --no-progress . 2>git-stderr.log && | ||||
|  | ||||
| 		grep "smudge write error at" git-stderr.log && | ||||
|  | @ -588,14 +588,14 @@ test_expect_success PERL 'process filter should restart after unexpected write f | |||
| 		cat >expected.log <<-EOF && | ||||
| 			START | ||||
| 			init handshake complete | ||||
| 			IN: smudge smudge-write-fail.r $SF [OK] -- OUT: $SF [WRITE FAIL] | ||||
| 			IN: smudge smudge-write-fail.r $SF [OK] -- [WRITE FAIL] | ||||
| 			START | ||||
| 			init handshake complete | ||||
| 			IN: smudge test.r $S [OK] -- OUT: $S . [OK] | ||||
| 			IN: smudge test2.r $S2 [OK] -- OUT: $S2 . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.r && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test2.o" test2.r && | ||||
|  | @ -609,7 +609,7 @@ test_expect_success PERL 'process filter should restart after unexpected write f | |||
| ' | ||||
|  | ||||
| test_expect_success PERL 'process filter should not be restarted if it signals an error' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean smudge" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean smudge" && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
|  | @ -634,12 +634,12 @@ test_expect_success PERL 'process filter should not be restarted if it signals a | |||
| 		cat >expected.log <<-EOF && | ||||
| 			START | ||||
| 			init handshake complete | ||||
| 			IN: smudge error.r $SE [OK] -- OUT: 0 [ERROR] | ||||
| 			IN: smudge error.r $SE [OK] -- [ERROR] | ||||
| 			IN: smudge test.r $S [OK] -- OUT: $S . [OK] | ||||
| 			IN: smudge test2.r $S2 [OK] -- OUT: $S2 . [OK] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.r && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test2.o" test2.r && | ||||
|  | @ -648,7 +648,7 @@ test_expect_success PERL 'process filter should not be restarted if it signals a | |||
| ' | ||||
|  | ||||
| test_expect_success PERL 'process filter abort stops processing of all further files' ' | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl clean smudge" && | ||||
| 	test_config_global filter.protocol.process "rot13-filter.pl debug.log clean smudge" && | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
|  | @ -673,10 +673,10 @@ test_expect_success PERL 'process filter abort stops processing of all further f | |||
| 		cat >expected.log <<-EOF && | ||||
| 			START | ||||
| 			init handshake complete | ||||
| 			IN: smudge abort.r $SA [OK] -- OUT: 0 [ABORT] | ||||
| 			IN: smudge abort.r $SA [OK] -- [ABORT] | ||||
| 			STOP | ||||
| 		EOF | ||||
| 		test_cmp_exclude_clean expected.log rot13-filter.log && | ||||
| 		test_cmp_exclude_clean expected.log debug.log && | ||||
|  | ||||
| 		test_cmp "$TEST_ROOT/test.o" test.r && | ||||
| 		test_cmp "$TEST_ROOT/test2.o" test2.r && | ||||
|  | @ -697,8 +697,124 @@ test_expect_success PERL 'invalid process filter must fail (and not hang!)' ' | |||
|  | ||||
| 		cp "$TEST_ROOT/test.o" test.r && | ||||
| 		test_must_fail git add . 2>git-stderr.log && | ||||
| 		grep "does not support filter protocol version" git-stderr.log | ||||
| 		grep "expected git-filter-server" git-stderr.log | ||||
| 	) | ||||
| ' | ||||
|  | ||||
| test_expect_success PERL 'delayed checkout in process filter' ' | ||||
| 	test_config_global filter.a.process "rot13-filter.pl a.log clean smudge delay" && | ||||
| 	test_config_global filter.a.required true && | ||||
| 	test_config_global filter.b.process "rot13-filter.pl b.log clean smudge delay" && | ||||
| 	test_config_global filter.b.required true && | ||||
|  | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
| 		cd repo && | ||||
| 		git init && | ||||
| 		echo "*.a filter=a" >.gitattributes && | ||||
| 		echo "*.b filter=b" >>.gitattributes && | ||||
| 		cp "$TEST_ROOT/test.o" test.a && | ||||
| 		cp "$TEST_ROOT/test.o" test-delay10.a && | ||||
| 		cp "$TEST_ROOT/test.o" test-delay11.a && | ||||
| 		cp "$TEST_ROOT/test.o" test-delay20.a && | ||||
| 		cp "$TEST_ROOT/test.o" test-delay10.b && | ||||
| 		git add . && | ||||
| 		git commit -m "test commit" | ||||
| 	) && | ||||
|  | ||||
| 	S=$(file_size "$TEST_ROOT/test.o") && | ||||
| 	cat >a.exp <<-EOF && | ||||
| 		START | ||||
| 		init handshake complete | ||||
| 		IN: smudge test.a $S [OK] -- OUT: $S . [OK] | ||||
| 		IN: smudge test-delay10.a $S [OK] -- [DELAYED] | ||||
| 		IN: smudge test-delay11.a $S [OK] -- [DELAYED] | ||||
| 		IN: smudge test-delay20.a $S [OK] -- [DELAYED] | ||||
| 		IN: list_available_blobs test-delay10.a test-delay11.a [OK] | ||||
| 		IN: smudge test-delay10.a 0 [OK] -- OUT: $S . [OK] | ||||
| 		IN: smudge test-delay11.a 0 [OK] -- OUT: $S . [OK] | ||||
| 		IN: list_available_blobs test-delay20.a [OK] | ||||
| 		IN: smudge test-delay20.a 0 [OK] -- OUT: $S . [OK] | ||||
| 		IN: list_available_blobs [OK] | ||||
| 		STOP | ||||
| 	EOF | ||||
| 	cat >b.exp <<-EOF && | ||||
| 		START | ||||
| 		init handshake complete | ||||
| 		IN: smudge test-delay10.b $S [OK] -- [DELAYED] | ||||
| 		IN: list_available_blobs test-delay10.b [OK] | ||||
| 		IN: smudge test-delay10.b 0 [OK] -- OUT: $S . [OK] | ||||
| 		IN: list_available_blobs [OK] | ||||
| 		STOP | ||||
| 	EOF | ||||
|  | ||||
| 	rm -rf repo-cloned && | ||||
| 	filter_git clone repo repo-cloned && | ||||
| 	test_cmp_count a.exp repo-cloned/a.log && | ||||
| 	test_cmp_count b.exp repo-cloned/b.log && | ||||
|  | ||||
| 	( | ||||
| 		cd repo-cloned && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay10.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay11.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay20.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay10.b && | ||||
|  | ||||
| 		rm *.a *.b && | ||||
| 		filter_git checkout . && | ||||
| 		test_cmp_count ../a.exp a.log && | ||||
| 		test_cmp_count ../b.exp b.log && | ||||
|  | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay10.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay11.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay20.a && | ||||
| 		test_cmp_committed_rot13 "$TEST_ROOT/test.o" test-delay10.b | ||||
| 	) | ||||
| ' | ||||
|  | ||||
| test_expect_success PERL 'missing file in delayed checkout' ' | ||||
| 	test_config_global filter.bug.process "rot13-filter.pl bug.log clean smudge delay" && | ||||
| 	test_config_global filter.bug.required true && | ||||
|  | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
| 		cd repo && | ||||
| 		git init && | ||||
| 		echo "*.a filter=bug" >.gitattributes && | ||||
| 		cp "$TEST_ROOT/test.o" missing-delay.a | ||||
| 		git add . && | ||||
| 		git commit -m "test commit" | ||||
| 	) && | ||||
|  | ||||
| 	rm -rf repo-cloned && | ||||
| 	test_must_fail git clone repo repo-cloned 2>git-stderr.log && | ||||
| 	cat git-stderr.log && | ||||
| 	grep "error: .missing-delay\.a. was not filtered properly" git-stderr.log | ||||
| ' | ||||
|  | ||||
| test_expect_success PERL 'invalid file in delayed checkout' ' | ||||
| 	test_config_global filter.bug.process "rot13-filter.pl bug.log clean smudge delay" && | ||||
| 	test_config_global filter.bug.required true && | ||||
|  | ||||
| 	rm -rf repo && | ||||
| 	mkdir repo && | ||||
| 	( | ||||
| 		cd repo && | ||||
| 		git init && | ||||
| 		echo "*.a filter=bug" >.gitattributes && | ||||
| 		cp "$TEST_ROOT/test.o" invalid-delay.a && | ||||
| 		cp "$TEST_ROOT/test.o" unfiltered | ||||
| 		git add . && | ||||
| 		git commit -m "test commit" | ||||
| 	) && | ||||
|  | ||||
| 	rm -rf repo-cloned && | ||||
| 	test_must_fail git clone repo repo-cloned 2>git-stderr.log && | ||||
| 	grep "error: external filter .* signaled that .unfiltered. is now available although it has not been delayed earlier" git-stderr.log | ||||
| ' | ||||
|  | ||||
| test_done | ||||
|  |  | |||
|  | @ -2,8 +2,9 @@ | |||
| # Example implementation for the Git filter protocol version 2 | ||||
| # See Documentation/gitattributes.txt, section "Filter Protocol" | ||||
| # | ||||
| # The script takes the list of supported protocol capabilities as | ||||
| # arguments ("clean", "smudge", etc). | ||||
| # The first argument defines a debug log file that the script write to. | ||||
| # All remaining arguments define a list of supported protocol | ||||
| # capabilities ("clean", "smudge", etc). | ||||
| # | ||||
| # This implementation supports special test cases: | ||||
| # (1) If data with the pathname "clean-write-fail.r" is processed with | ||||
|  | @ -17,6 +18,16 @@ | |||
| #     operation then the filter signals that it cannot or does not want | ||||
| #     to process the file and any file after that is processed with the | ||||
| #     same command. | ||||
| # (5) If data with a pathname that is a key in the DELAY hash is | ||||
| #     requested (e.g. "test-delay10.a") then the filter responds with | ||||
| #     a "delay" status and sets the "requested" field in the DELAY hash. | ||||
| #     The filter will signal the availability of this object after | ||||
| #     "count" (field in DELAY hash) "list_available_blobs" commands. | ||||
| # (6) If data with the pathname "missing-delay.a" is processed that the | ||||
| #     filter will drop the path from the "list_available_blobs" response. | ||||
| # (7) If data with the pathname "invalid-delay.a" is processed that the | ||||
| #     filter will add the path "unfiltered" which was not delayed before | ||||
| #     to the "list_available_blobs" response. | ||||
| # | ||||
|  | ||||
| use strict; | ||||
|  | @ -24,9 +35,19 @@ use warnings; | |||
| use IO::File; | ||||
|  | ||||
| my $MAX_PACKET_CONTENT_SIZE = 65516; | ||||
| my $log_file                = shift @ARGV; | ||||
| my @capabilities            = @ARGV; | ||||
|  | ||||
| open my $debug, ">>", "rot13-filter.log" or die "cannot open log file: $!"; | ||||
| open my $debug, ">>", $log_file or die "cannot open log file: $!"; | ||||
|  | ||||
| my %DELAY = ( | ||||
| 	'test-delay10.a' => { "requested" => 0, "count" => 1 }, | ||||
| 	'test-delay11.a' => { "requested" => 0, "count" => 1 }, | ||||
| 	'test-delay20.a' => { "requested" => 0, "count" => 2 }, | ||||
| 	'test-delay10.b' => { "requested" => 0, "count" => 1 }, | ||||
| 	'missing-delay.a' => { "requested" => 0, "count" => 1 }, | ||||
| 	'invalid-delay.a' => { "requested" => 0, "count" => 1 }, | ||||
| ); | ||||
|  | ||||
| sub rot13 { | ||||
| 	my $str = shift; | ||||
|  | @ -64,7 +85,7 @@ sub packet_bin_read { | |||
|  | ||||
| sub packet_txt_read { | ||||
| 	my ( $res, $buf ) = packet_bin_read(); | ||||
| 	unless ( $buf =~ s/\n$// ) { | ||||
| 	unless ( $buf eq '' or $buf =~ s/\n$// ) { | ||||
| 		die "A non-binary line MUST be terminated by an LF."; | ||||
| 	} | ||||
| 	return ( $res, $buf ); | ||||
|  | @ -99,6 +120,7 @@ packet_flush(); | |||
|  | ||||
| ( packet_txt_read() eq ( 0, "capability=clean" ) )  || die "bad capability"; | ||||
| ( packet_txt_read() eq ( 0, "capability=smudge" ) ) || die "bad capability"; | ||||
| ( packet_txt_read() eq ( 0, "capability=delay" ) )  || die "bad capability"; | ||||
| ( packet_bin_read() eq ( 1, "" ) )                  || die "bad capability end"; | ||||
|  | ||||
| foreach (@capabilities) { | ||||
|  | @ -113,6 +135,34 @@ while (1) { | |||
| 	print $debug "IN: $command"; | ||||
| 	$debug->flush(); | ||||
|  | ||||
| 	if ( $command eq "list_available_blobs" ) { | ||||
| 		# Flush | ||||
| 		packet_bin_read(); | ||||
|  | ||||
| 		foreach my $pathname ( sort keys %DELAY ) { | ||||
| 			if ( $DELAY{$pathname}{"requested"} >= 1 ) { | ||||
| 				$DELAY{$pathname}{"count"} = $DELAY{$pathname}{"count"} - 1; | ||||
| 				if ( $pathname eq "invalid-delay.a" ) { | ||||
| 					# Send Git a pathname that was not delayed earlier | ||||
| 					packet_txt_write("pathname=unfiltered"); | ||||
| 				} | ||||
| 				if ( $pathname eq "missing-delay.a" ) { | ||||
| 					# Do not signal Git that this file is available | ||||
| 				} elsif ( $DELAY{$pathname}{"count"} == 0 ) { | ||||
| 					print $debug " $pathname"; | ||||
| 					packet_txt_write("pathname=$pathname"); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		packet_flush(); | ||||
|  | ||||
| 		print $debug " [OK]\n"; | ||||
| 		$debug->flush(); | ||||
| 		packet_txt_write("status=success"); | ||||
| 		packet_flush(); | ||||
| 	} | ||||
| 	else { | ||||
| 		my ( $pathname ) = packet_txt_read() =~ /^pathname=(.+)$/; | ||||
| 		print $debug " $pathname"; | ||||
| 		$debug->flush(); | ||||
|  | @ -121,8 +171,19 @@ while (1) { | |||
| 			die "bad pathname '$pathname'"; | ||||
| 		} | ||||
|  | ||||
| 	# Flush | ||||
| 	packet_bin_read(); | ||||
| 		# Read until flush | ||||
| 		my ( $done, $buffer ) = packet_txt_read(); | ||||
| 		while ( $buffer ne '' ) { | ||||
| 			if ( $buffer eq "can-delay=1" ) { | ||||
| 				if ( exists $DELAY{$pathname} and $DELAY{$pathname}{"requested"} == 0 ) { | ||||
| 					$DELAY{$pathname}{"requested"} = 1; | ||||
| 				} | ||||
| 			} else { | ||||
| 				die "Unknown message '$buffer'"; | ||||
| 			} | ||||
|  | ||||
| 			( $done, $buffer ) = packet_txt_read(); | ||||
| 		} | ||||
|  | ||||
| 		my $input = ""; | ||||
| 		{ | ||||
|  | @ -138,7 +199,10 @@ while (1) { | |||
| 		} | ||||
|  | ||||
| 		my $output; | ||||
| 	if ( $pathname eq "error.r" or $pathname eq "abort.r" ) { | ||||
| 		if ( exists $DELAY{$pathname} and exists $DELAY{$pathname}{"output"} ) { | ||||
| 			$output = $DELAY{$pathname}{"output"} | ||||
| 		} | ||||
| 		elsif ( $pathname eq "error.r" or $pathname eq "abort.r" ) { | ||||
| 			$output = ""; | ||||
| 		} | ||||
| 		elsif ( $command eq "clean" and grep( /^clean$/, @capabilities ) ) { | ||||
|  | @ -151,9 +215,6 @@ while (1) { | |||
| 			die "bad command '$command'"; | ||||
| 		} | ||||
|  | ||||
| 	print $debug "OUT: " . length($output) . " "; | ||||
| 	$debug->flush(); | ||||
|  | ||||
| 		if ( $pathname eq "error.r" ) { | ||||
| 			print $debug "[ERROR]\n"; | ||||
| 			$debug->flush(); | ||||
|  | @ -166,6 +227,17 @@ while (1) { | |||
| 			packet_txt_write("status=abort"); | ||||
| 			packet_flush(); | ||||
| 		} | ||||
| 		elsif ( $command eq "smudge" and | ||||
| 			exists $DELAY{$pathname} and | ||||
| 			$DELAY{$pathname}{"requested"} == 1 | ||||
| 		) { | ||||
| 			print $debug "[DELAYED]\n"; | ||||
| 			$debug->flush(); | ||||
| 			packet_txt_write("status=delayed"); | ||||
| 			packet_flush(); | ||||
| 			$DELAY{$pathname}{"requested"} = 2; | ||||
| 			$DELAY{$pathname}{"output"} = $output; | ||||
| 		} | ||||
| 		else { | ||||
| 			packet_txt_write("status=success"); | ||||
| 			packet_flush(); | ||||
|  | @ -176,6 +248,9 @@ while (1) { | |||
| 				die "${command} write error"; | ||||
| 			} | ||||
|  | ||||
| 			print $debug "OUT: " . length($output) . " "; | ||||
| 			$debug->flush(); | ||||
|  | ||||
| 			while ( length($output) > 0 ) { | ||||
| 				my $packet = substr( $output, 0, $MAX_PACKET_CONTENT_SIZE ); | ||||
| 				packet_bin_write($packet); | ||||
|  | @ -194,3 +269,4 @@ while (1) { | |||
| 			packet_flush(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -380,6 +380,7 @@ static int check_updates(struct unpack_trees_options *o) | |||
| 	if (should_update_submodules() && o->update && !o->dry_run) | ||||
| 		reload_gitmodules_file(index, &state); | ||||
|  | ||||
| 	enable_delayed_checkout(&state); | ||||
| 	for (i = 0; i < index->cache_nr; i++) { | ||||
| 		struct cache_entry *ce = index->cache[i]; | ||||
|  | ||||
|  | @ -394,6 +395,7 @@ static int check_updates(struct unpack_trees_options *o) | |||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	errs |= finish_delayed_checkout(&state); | ||||
| 	stop_progress(&progress); | ||||
| 	if (o->update) | ||||
| 		git_attr_set_direction(GIT_ATTR_CHECKIN, NULL); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Junio C Hamano
						Junio C Hamano