Skip to content

Commit aa4ed40

Browse files
author
Junio C Hamano
committed
Add 'filter' attribute and external filter driver definition.
The interface is similar to the custom low-level merge drivers. First you configure your filter driver by defining 'filter.<name>.*' variables in the configuration. filter.<name>.clean filter command to run upon checkin filter.<name>.smudge filter command to run upon checkout Then you assign filter attribute to each path, whose name matches the custom filter driver's name. Example: (in .gitattributes) *.c filter=indent (in config) [filter "indent"] clean = indent smudge = cat Signed-off-by: Junio C Hamano <junkio@cox.net>
1 parent 3fed15f commit aa4ed40

File tree

3 files changed

+282
-3
lines changed

3 files changed

+282
-3
lines changed

Documentation/gitattributes.txt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,45 @@ In the check-out codepath, the blob content is first converted
156156
with `crlf`, and then `ident`.
157157

158158

159+
`filter`
160+
^^^^^^^^
161+
162+
A `filter` attribute can be set to a string value. This names
163+
filter driver specified in the configuration.
164+
165+
A filter driver consists of `clean` command and `smudge`
166+
command, either of which can be left unspecified. Upon
167+
checkout, when `smudge` command is specified, the command is fed
168+
the blob object from its standard input, and its standard output
169+
is used to update the worktree file. Similarly, `clean` command
170+
is used to convert the contents of worktree file upon checkin.
171+
172+
Missing filter driver definition in the config is not an error
173+
but makes the filter a no-op passthru.
174+
175+
The content filtering is done to massage the content into a
176+
shape that is more convenient for the platform, filesystem, and
177+
the user to use. The keyword here is "more convenient" and not
178+
"turning something unusable into usable". In other words, it is
179+
"hanging yourself because we gave you a long rope" if your
180+
project uses filtering mechanism in such a way that it makes
181+
your project unusable unless the checkout is done with a
182+
specific filter in effect.
183+
184+
185+
Interaction between checkin/checkout attributes
186+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
187+
188+
In the check-in codepath, the worktree file is first converted
189+
with `filter` driver (if specified and corresponding driver
190+
defined), then the result is processed with `ident` (if
191+
specified), and then finally with `crlf` (again, if specified
192+
and applicable).
193+
194+
In the check-out codepath, the blob content is first converted
195+
with `crlf`, and then `ident` and fed to `filter`.
196+
197+
159198
Generating diff text
160199
~~~~~~~~~~~~~~~~~~~~
161200

convert.c

Lines changed: 234 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,212 @@ static char *crlf_to_worktree(const char *path, const char *src, unsigned long *
201201
return buffer;
202202
}
203203

204+
static int filter_buffer(const char *path, const char *src,
205+
unsigned long size, const char *cmd)
206+
{
207+
/*
208+
* Spawn cmd and feed the buffer contents through its stdin.
209+
*/
210+
struct child_process child_process;
211+
int pipe_feed[2];
212+
int write_err, status;
213+
214+
memset(&child_process, 0, sizeof(child_process));
215+
216+
if (pipe(pipe_feed) < 0) {
217+
error("cannot create pipe to run external filter %s", cmd);
218+
return 1;
219+
}
220+
221+
child_process.pid = fork();
222+
if (child_process.pid < 0) {
223+
error("cannot fork to run external filter %s", cmd);
224+
close(pipe_feed[0]);
225+
close(pipe_feed[1]);
226+
return 1;
227+
}
228+
if (!child_process.pid) {
229+
dup2(pipe_feed[0], 0);
230+
close(pipe_feed[0]);
231+
close(pipe_feed[1]);
232+
execlp("sh", "sh", "-c", cmd, NULL);
233+
return 1;
234+
}
235+
close(pipe_feed[0]);
236+
237+
write_err = (write_in_full(pipe_feed[1], src, size) < 0);
238+
if (close(pipe_feed[1]))
239+
write_err = 1;
240+
if (write_err)
241+
error("cannot feed the input to external filter %s", cmd);
242+
243+
status = finish_command(&child_process);
244+
if (status)
245+
error("external filter %s failed %d", cmd, -status);
246+
return (write_err || status);
247+
}
248+
249+
static char *apply_filter(const char *path, const char *src,
250+
unsigned long *sizep, const char *cmd)
251+
{
252+
/*
253+
* Create a pipeline to have the command filter the buffer's
254+
* contents.
255+
*
256+
* (child --> cmd) --> us
257+
*/
258+
const int SLOP = 4096;
259+
int pipe_feed[2];
260+
int status;
261+
char *dst;
262+
unsigned long dstsize, dstalloc;
263+
struct child_process child_process;
264+
265+
if (!cmd)
266+
return NULL;
267+
268+
memset(&child_process, 0, sizeof(child_process));
269+
270+
if (pipe(pipe_feed) < 0) {
271+
error("cannot create pipe to run external filter %s", cmd);
272+
return NULL;
273+
}
274+
275+
fflush(NULL);
276+
child_process.pid = fork();
277+
if (child_process.pid < 0) {
278+
error("cannot fork to run external filter %s", cmd);
279+
close(pipe_feed[0]);
280+
close(pipe_feed[1]);
281+
return NULL;
282+
}
283+
if (!child_process.pid) {
284+
dup2(pipe_feed[1], 1);
285+
close(pipe_feed[0]);
286+
close(pipe_feed[1]);
287+
exit(filter_buffer(path, src, *sizep, cmd));
288+
}
289+
close(pipe_feed[1]);
290+
291+
dstalloc = *sizep;
292+
dst = xmalloc(dstalloc);
293+
dstsize = 0;
294+
295+
while (1) {
296+
ssize_t numread = xread(pipe_feed[0], dst + dstsize,
297+
dstalloc - dstsize);
298+
299+
if (numread <= 0) {
300+
if (!numread)
301+
break;
302+
error("read from external filter %s failed", cmd);
303+
free(dst);
304+
dst = NULL;
305+
break;
306+
}
307+
dstsize += numread;
308+
if (dstalloc <= dstsize + SLOP) {
309+
dstalloc = dstsize + SLOP;
310+
dst = xrealloc(dst, dstalloc);
311+
}
312+
}
313+
if (close(pipe_feed[0])) {
314+
error("read from external filter %s failed", cmd);
315+
free(dst);
316+
dst = NULL;
317+
}
318+
319+
status = finish_command(&child_process);
320+
if (status) {
321+
error("external filter %s failed %d", cmd, -status);
322+
free(dst);
323+
dst = NULL;
324+
}
325+
326+
if (dst)
327+
*sizep = dstsize;
328+
return dst;
329+
}
330+
331+
static struct convert_driver {
332+
const char *name;
333+
struct convert_driver *next;
334+
char *smudge;
335+
char *clean;
336+
} *user_convert, **user_convert_tail;
337+
338+
static int read_convert_config(const char *var, const char *value)
339+
{
340+
const char *ep, *name;
341+
int namelen;
342+
struct convert_driver *drv;
343+
344+
/*
345+
* External conversion drivers are configured using
346+
* "filter.<name>.variable".
347+
*/
348+
if (prefixcmp(var, "filter.") || (ep = strrchr(var, '.')) == var + 6)
349+
return 0;
350+
name = var + 7;
351+
namelen = ep - name;
352+
for (drv = user_convert; drv; drv = drv->next)
353+
if (!strncmp(drv->name, name, namelen) && !drv->name[namelen])
354+
break;
355+
if (!drv) {
356+
char *namebuf;
357+
drv = xcalloc(1, sizeof(struct convert_driver));
358+
namebuf = xmalloc(namelen + 1);
359+
memcpy(namebuf, name, namelen);
360+
namebuf[namelen] = 0;
361+
drv->name = namebuf;
362+
drv->next = NULL;
363+
*user_convert_tail = drv;
364+
user_convert_tail = &(drv->next);
365+
}
366+
367+
ep++;
368+
369+
/*
370+
* filter.<name>.smudge and filter.<name>.clean specifies
371+
* the command line:
372+
*
373+
* command-line
374+
*
375+
* The command-line will not be interpolated in any way.
376+
*/
377+
378+
if (!strcmp("smudge", ep)) {
379+
if (!value)
380+
return error("%s: lacks value", var);
381+
drv->smudge = strdup(value);
382+
return 0;
383+
}
384+
385+
if (!strcmp("clean", ep)) {
386+
if (!value)
387+
return error("%s: lacks value", var);
388+
drv->clean = strdup(value);
389+
return 0;
390+
}
391+
return 0;
392+
}
393+
204394
static void setup_convert_check(struct git_attr_check *check)
205395
{
206396
static struct git_attr *attr_crlf;
207397
static struct git_attr *attr_ident;
398+
static struct git_attr *attr_filter;
208399

209400
if (!attr_crlf) {
210401
attr_crlf = git_attr("crlf", 4);
211402
attr_ident = git_attr("ident", 5);
403+
attr_filter = git_attr("filter", 6);
404+
user_convert_tail = &user_convert;
405+
git_config(read_convert_config);
212406
}
213407
check[0].attr = attr_crlf;
214408
check[1].attr = attr_ident;
409+
check[2].attr = attr_filter;
215410
}
216411

217412
static int count_ident(const char *cp, unsigned long size)
@@ -367,6 +562,20 @@ static int git_path_check_crlf(const char *path, struct git_attr_check *check)
367562
return CRLF_GUESS;
368563
}
369564

565+
static struct convert_driver *git_path_check_convert(const char *path,
566+
struct git_attr_check *check)
567+
{
568+
const char *value = check->value;
569+
struct convert_driver *drv;
570+
571+
if (ATTR_TRUE(value) || ATTR_FALSE(value) || ATTR_UNSET(value))
572+
return NULL;
573+
for (drv = user_convert; drv; drv = drv->next)
574+
if (!strcmp(value, drv->name))
575+
return drv;
576+
return NULL;
577+
}
578+
370579
static int git_path_check_ident(const char *path, struct git_attr_check *check)
371580
{
372581
const char *value = check->value;
@@ -376,18 +585,29 @@ static int git_path_check_ident(const char *path, struct git_attr_check *check)
376585

377586
char *convert_to_git(const char *path, const char *src, unsigned long *sizep)
378587
{
379-
struct git_attr_check check[2];
588+
struct git_attr_check check[3];
380589
int crlf = CRLF_GUESS;
381590
int ident = 0;
591+
char *filter = NULL;
382592
char *buf, *buf2;
383593

384594
setup_convert_check(check);
385595
if (!git_checkattr(path, ARRAY_SIZE(check), check)) {
596+
struct convert_driver *drv;
386597
crlf = git_path_check_crlf(path, check + 0);
387598
ident = git_path_check_ident(path, check + 1);
599+
drv = git_path_check_convert(path, check + 2);
600+
if (drv && drv->clean)
601+
filter = drv->clean;
388602
}
389603

390-
buf = crlf_to_git(path, src, sizep, crlf);
604+
buf = apply_filter(path, src, sizep, filter);
605+
606+
buf2 = crlf_to_git(path, buf ? buf : src, sizep, crlf);
607+
if (buf2) {
608+
free(buf);
609+
buf = buf2;
610+
}
391611

392612
buf2 = ident_to_git(path, buf ? buf : src, sizep, ident);
393613
if (buf2) {
@@ -400,15 +620,20 @@ char *convert_to_git(const char *path, const char *src, unsigned long *sizep)
400620

401621
char *convert_to_working_tree(const char *path, const char *src, unsigned long *sizep)
402622
{
403-
struct git_attr_check check[2];
623+
struct git_attr_check check[3];
404624
int crlf = CRLF_GUESS;
405625
int ident = 0;
626+
char *filter = NULL;
406627
char *buf, *buf2;
407628

408629
setup_convert_check(check);
409630
if (!git_checkattr(path, ARRAY_SIZE(check), check)) {
631+
struct convert_driver *drv;
410632
crlf = git_path_check_crlf(path, check + 0);
411633
ident = git_path_check_ident(path, check + 1);
634+
drv = git_path_check_convert(path, check + 2);
635+
if (drv && drv->smudge)
636+
filter = drv->smudge;
412637
}
413638

414639
buf = ident_to_worktree(path, src, sizep, ident);
@@ -419,5 +644,11 @@ char *convert_to_working_tree(const char *path, const char *src, unsigned long *
419644
buf = buf2;
420645
}
421646

647+
buf2 = apply_filter(path, buf ? buf : src, sizep, filter);
648+
if (buf2) {
649+
free(buf);
650+
buf = buf2;
651+
}
652+
422653
return buf;
423654
}

t/t0021-conversion.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@ test_description='blob conversion via gitattributes'
44

55
. ./test-lib.sh
66

7+
cat <<\EOF >rot13.sh
8+
tr '[a-zA-Z]' '[n-za-mN-ZA-M]'
9+
EOF
10+
chmod +x rot13.sh
11+
712
test_expect_success setup '
13+
git config filter.rot13.smudge ./rot13.sh &&
14+
git config filter.rot13.clean ./rot13.sh &&
15+
816
{
17+
echo "*.t filter=rot13"
918
echo "*.i ident"
1019
} >.gitattributes &&
1120

0 commit comments

Comments
 (0)