Fossil SCM

Add the --include and --exclude options to the tarball and zip commands and the in= and ex= query parameters to the /tarball and /zip webpages.

drh 2016-06-10 12:46 trunk
Commit ef449a1173c556b2a25d084de6408bd53b5da67c
2 files changed +94 -32 +83 -23
+94 -32
--- src/tar.c
+++ src/tar.c
@@ -73,11 +73,11 @@
7373
);
7474
}
7575
7676
7777
/*
78
-** verify that lla characters in 'zName' are in the
78
+** Verify that all characters in 'zName' are in the
7979
** ISO646 (=ASCII) character set.
8080
*/
8181
static int is_iso646_name(
8282
const char *zName, /* file path */
8383
int nName /* path length */
@@ -90,11 +90,11 @@
9090
return 1;
9191
}
9292
9393
9494
/*
95
-** copy string pSrc into pDst, truncating or padding with 0 if necessary
95
+** copy string pSrc into pDst, truncating or padding with 0 if necessary
9696
*/
9797
static void padded_copy(
9898
char *pDest,
9999
int nDest,
100100
const char *pSrc,
@@ -446,15 +446,16 @@
446446
blob_write_to_file(&zip, g.argv[2]);
447447
}
448448
449449
/*
450450
** Given the RID for a check-in, construct a tarball containing
451
-** all files in that check-in
451
+** all files in that check-in that match pGlob (or all files if
452
+** pGlob is NULL).
452453
**
453454
** If RID is for an object that is not a real manifest, then the
454455
** resulting tarball contains a single file which is the RID
455
-** object.
456
+** object. pInclude and pExclude are ignored in this case.
456457
**
457458
** If the RID object does not exist in the repository, then
458459
** pTar is zeroed.
459460
**
460461
** zDir is a "synthetic" subdirectory which all files get
@@ -462,11 +463,17 @@
462463
** which case it is ignored. The intention is to create a tarball which
463464
** politely expands into a subdir instead of filling your current dir
464465
** with source files. For example, pass a UUID or "ProjectName".
465466
**
466467
*/
467
-void tarball_of_checkin(int rid, Blob *pTar, const char *zDir){
468
+void tarball_of_checkin(
469
+ int rid, /* The RID of the checkin from which to form a tarball */
470
+ Blob *pTar, /* Write the tarball into this blob */
471
+ const char *zDir, /* Directory prefix for all file added to tarball */
472
+ Glob *pInclude, /* Only add files matching this pattern */
473
+ Glob *pExclude /* Exclude files matching this pattern */
474
+){
468475
Blob mfile, hash, file;
469476
Manifest *pManifest;
470477
ManifestFile *pFile;
471478
Blob filename;
472479
int nPrefix;
@@ -488,11 +495,14 @@
488495
489496
pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
490497
if( pManifest ){
491498
mTime = (pManifest->rDate - 2440587.5)*86400.0;
492499
tar_begin(mTime);
493
- if( db_get_boolean("manifest", 0) ){
500
+ if( (pInclude==0 || glob_match(pInclude, "manifest"))
501
+ && !glob_match(pExclude, "manifest")
502
+ && db_get_boolean("manifest", 0)
503
+ ){
494504
blob_append(&filename, "manifest", -1);
495505
zName = blob_str(&filename);
496506
sha1sum_blob(&mfile, &hash);
497507
sterilize_manifest(&mfile);
498508
tar_add_file(zName, &mfile, 0, mTime);
@@ -504,11 +514,14 @@
504514
tar_add_file(zName, &hash, 0, mTime);
505515
blob_reset(&hash);
506516
}
507517
manifest_file_rewind(pManifest);
508518
while( (pFile = manifest_file_next(pManifest,0))!=0 ){
509
- int fid = uuid_to_rid(pFile->zUuid, 0);
519
+ int fid;
520
+ if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue;
521
+ if( glob_match(pExclude, pFile->zName) ) continue;
522
+ fid = uuid_to_rid(pFile->zUuid, 0);
510523
if( fid ){
511524
content_get(fid, &file);
512525
blob_resize(&filename, nPrefix);
513526
blob_append(&filename, pFile->zName, -1);
514527
zName = blob_str(&filename);
@@ -539,19 +552,34 @@
539552
** option is used, its argument becomes the name of the top-level directory
540553
** in the resulting tarball. If --name is omitted, the top-level directory
541554
** named is derived from the project name, the check-in date and time, and
542555
** the artifact ID of the check-in.
543556
**
557
+** The GLOBLIST argument to --exclude and --include can be a comma-separated
558
+** list of glob patterns, where each glob pattern may optionally be enclosed
559
+** in "..." or '...' so that it may contain commas. If a file matches both
560
+** --include and --exclude then it is excluded.
561
+**
544562
** Options:
545
-** --name DIRECTORYNAME The name of the top-level directory in the archive
546
-** -R REPOSITORY Specify a Fossil repository
563
+** -X|--exclude GLOBLIST Comma-separated list of GLOBs of files to exclude
564
+** --include GLOBLIST Comma-separated list of GLOBs of files to include
565
+** --name DIRECTORYNAME The name of the top-level directory in the archive
566
+** -R REPOSITORY Specify a Fossil repository
547567
*/
548568
void tarball_cmd(void){
549569
int rid;
550570
Blob tarball;
551571
const char *zName;
572
+ Glob *pInclude = 0;
573
+ Glob *pExclude = 0;
574
+ const char *zInclude;
575
+ const char *zExclude;
552576
zName = find_option("name", 0, 1);
577
+ zExclude = find_option("exclude", "X", 1);
578
+ if( zExclude ) pExclude = glob_create(zExclude);
579
+ zInclude = find_option("include", 0, 1);
580
+ if( zInclude ) pInclude = glob_create(zInclude);
553581
db_find_and_open_repository(0, 0);
554582
555583
/* We should be done with options.. */
556584
verify_all_options();
557585
@@ -573,46 +601,65 @@
573601
" WHERE event.objid=%d"
574602
" AND blob.rid=%d",
575603
db_get("project-name", "unnamed"), rid, rid
576604
);
577605
}
578
- tarball_of_checkin(rid, &tarball, zName);
606
+ tarball_of_checkin(rid, &tarball, zName, pInclude, pExclude);
607
+ glob_free(pInclude);
608
+ glob_free(pExclude);
579609
blob_write_to_file(&tarball, g.argv[3]);
580610
blob_reset(&tarball);
581611
}
582612
583613
/*
584614
** WEBPAGE: tarball
585
-** URL: /tarball/RID.tar.gz
586
-**
587
-** Generate a compressed tarball for a check-in.
588
-** Return that tarball as the HTTP reply content.
589
-**
590
-** Optional URL Parameters:
591
-**
592
-** - name=NAME[.tar.gz] is base name of the output file. Defaults to
593
-** something project/version-specific. The prefix of the name, up to
594
-** the last '.', are used as the top-most directory name in the tar
595
-** output.
596
-**
597
-** - uuid=the version to tar (may be a tag/branch name).
598
-** Defaults to "trunk".
599
-**
615
+** URL: /tarball
616
+**
617
+** Generate a compressed tarball for a check-in and return that
618
+** tarball as the HTTP reply content.
619
+**
620
+** Query parameters:
621
+**
622
+** name=NAME[.tar.gz] The base name of the output file. The default
623
+** value is a configuration parameter in the project
624
+** settings. A prefix of the name, omitting the extension,
625
+** is used as the top-most directory name.
626
+**
627
+** uuid=TAG The check-in that is turned into a tarball.
628
+** Defaults to "trunk".
629
+**
630
+** in=PATTERN Only include files that match the comma-separate
631
+** list of GLOB patterns in PATTERN, as with ex=
632
+**
633
+** ex=PATTERN Omit any file that match PATTERN. PATTERN is a
634
+** comma-separated list of GLOB patterns, where each
635
+** pattern can optionally be quoted using ".." or '..'.
636
+** Any file matching both ex= and in= is excluded.
600637
*/
601638
void tarball_page(void){
602639
int rid;
603640
char *zName, *zRid, *zKey;
604641
int nName, nRid;
605
- Blob tarball;
642
+ const char *zInclude; /* The in= query parameter */
643
+ const char *zExclude; /* The ex= query parameter */
644
+ Blob cacheKey; /* The key to cache */
645
+ Glob *pInclude = 0; /* The compiled in= glob pattern */
646
+ Glob *pExclude = 0; /* The compiled ex= glob pattern */
647
+ Blob tarball; /* Tarball accumulated here */
606648
607649
login_check_credentials();
608650
if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
609651
load_control();
610652
zName = mprintf("%s", PD("name",""));
611653
nName = strlen(zName);
612654
zRid = mprintf("%s", PD("uuid","trunk"));
613655
nRid = strlen(zRid);
656
+ zInclude = P("in");
657
+ if( zInclude ) pInclude = glob_create(zInclude);
658
+ zExclude = P("ex");
659
+ if( zExclude ) pExclude = glob_create(zExclude);
660
+
614661
if( nName>7 && fossil_strcmp(&zName[nName-7], ".tar.gz")==0 ){
615662
/* Special case: Remove the ".tar.gz" suffix. */
616663
nName -= 7;
617664
zName[nName] = 0;
618665
}else{
@@ -629,16 +676,29 @@
629676
if( rid==0 ){
630677
@ Not found
631678
return;
632679
}
633680
if( nRid==0 && nName>10 ) zName[10] = 0;
634
- zKey = db_text(0, "SELECT '/tarball/'||uuid||'/%q'"
635
- " FROM blob WHERE rid=%d",zName,rid);
681
+
682
+ /* Compute a unique key for the cache entry based on query parameters */
683
+ blob_init(&cacheKey, 0, 0);
684
+ blob_appendf(&cacheKey, "/tarball/%z", rid_to_uuid(rid));
685
+ if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude);
686
+ if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude);
687
+ blob_appendf(&cacheKey, "/%q", zName);
688
+ zKey = blob_str(&cacheKey);
689
+
636690
if( P("debug")!=0 ){
637691
style_header("Tarball Generator Debug Screen");
638692
@ zName = "%h(zName)"<br>
639693
@ rid = %d(rid)<br>
694
+ if( zInclude ){
695
+ @ zInclude = "%h(zInclude)"<br>
696
+ }
697
+ if( zExclude ){
698
+ @ zExclude = "%h(zExclude)"<br>
699
+ }
640700
@ zKey = "%h(zKey)"
641701
style_footer();
642702
return;
643703
}
644704
if( referred_from_login() ){
@@ -652,14 +712,16 @@
652712
style_footer();
653713
return;
654714
}
655715
blob_zero(&tarball);
656716
if( cache_read(&tarball, zKey)==0 ){
657
- tarball_of_checkin(rid, &tarball, zName);
717
+ tarball_of_checkin(rid, &tarball, zName, pInclude, pExclude);
658718
cache_write(&tarball, zKey);
659719
}
660
- free( zName );
661
- free( zRid );
662
- free( zKey );
720
+ glob_free(pInclude);
721
+ glob_free(pExclude);
722
+ fossil_free(zName);
723
+ fossil_free(zRid);
724
+ blob_reset(&cacheKey);
663725
cgi_set_content(&tarball);
664726
cgi_set_content_type("application/x-compressed");
665727
}
666728
--- src/tar.c
+++ src/tar.c
@@ -73,11 +73,11 @@
73 );
74 }
75
76
77 /*
78 ** verify that lla characters in 'zName' are in the
79 ** ISO646 (=ASCII) character set.
80 */
81 static int is_iso646_name(
82 const char *zName, /* file path */
83 int nName /* path length */
@@ -90,11 +90,11 @@
90 return 1;
91 }
92
93
94 /*
95 ** copy string pSrc into pDst, truncating or padding with 0 if necessary
96 */
97 static void padded_copy(
98 char *pDest,
99 int nDest,
100 const char *pSrc,
@@ -446,15 +446,16 @@
446 blob_write_to_file(&zip, g.argv[2]);
447 }
448
449 /*
450 ** Given the RID for a check-in, construct a tarball containing
451 ** all files in that check-in
 
452 **
453 ** If RID is for an object that is not a real manifest, then the
454 ** resulting tarball contains a single file which is the RID
455 ** object.
456 **
457 ** If the RID object does not exist in the repository, then
458 ** pTar is zeroed.
459 **
460 ** zDir is a "synthetic" subdirectory which all files get
@@ -462,11 +463,17 @@
462 ** which case it is ignored. The intention is to create a tarball which
463 ** politely expands into a subdir instead of filling your current dir
464 ** with source files. For example, pass a UUID or "ProjectName".
465 **
466 */
467 void tarball_of_checkin(int rid, Blob *pTar, const char *zDir){
 
 
 
 
 
 
468 Blob mfile, hash, file;
469 Manifest *pManifest;
470 ManifestFile *pFile;
471 Blob filename;
472 int nPrefix;
@@ -488,11 +495,14 @@
488
489 pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
490 if( pManifest ){
491 mTime = (pManifest->rDate - 2440587.5)*86400.0;
492 tar_begin(mTime);
493 if( db_get_boolean("manifest", 0) ){
 
 
 
494 blob_append(&filename, "manifest", -1);
495 zName = blob_str(&filename);
496 sha1sum_blob(&mfile, &hash);
497 sterilize_manifest(&mfile);
498 tar_add_file(zName, &mfile, 0, mTime);
@@ -504,11 +514,14 @@
504 tar_add_file(zName, &hash, 0, mTime);
505 blob_reset(&hash);
506 }
507 manifest_file_rewind(pManifest);
508 while( (pFile = manifest_file_next(pManifest,0))!=0 ){
509 int fid = uuid_to_rid(pFile->zUuid, 0);
 
 
 
510 if( fid ){
511 content_get(fid, &file);
512 blob_resize(&filename, nPrefix);
513 blob_append(&filename, pFile->zName, -1);
514 zName = blob_str(&filename);
@@ -539,19 +552,34 @@
539 ** option is used, its argument becomes the name of the top-level directory
540 ** in the resulting tarball. If --name is omitted, the top-level directory
541 ** named is derived from the project name, the check-in date and time, and
542 ** the artifact ID of the check-in.
543 **
 
 
 
 
 
544 ** Options:
545 ** --name DIRECTORYNAME The name of the top-level directory in the archive
546 ** -R REPOSITORY Specify a Fossil repository
 
 
547 */
548 void tarball_cmd(void){
549 int rid;
550 Blob tarball;
551 const char *zName;
 
 
 
 
552 zName = find_option("name", 0, 1);
 
 
 
 
553 db_find_and_open_repository(0, 0);
554
555 /* We should be done with options.. */
556 verify_all_options();
557
@@ -573,46 +601,65 @@
573 " WHERE event.objid=%d"
574 " AND blob.rid=%d",
575 db_get("project-name", "unnamed"), rid, rid
576 );
577 }
578 tarball_of_checkin(rid, &tarball, zName);
 
 
579 blob_write_to_file(&tarball, g.argv[3]);
580 blob_reset(&tarball);
581 }
582
583 /*
584 ** WEBPAGE: tarball
585 ** URL: /tarball/RID.tar.gz
586 **
587 ** Generate a compressed tarball for a check-in.
588 ** Return that tarball as the HTTP reply content.
589 **
590 ** Optional URL Parameters:
591 **
592 ** - name=NAME[.tar.gz] is base name of the output file. Defaults to
593 ** something project/version-specific. The prefix of the name, up to
594 ** the last '.', are used as the top-most directory name in the tar
595 ** output.
596 **
597 ** - uuid=the version to tar (may be a tag/branch name).
598 ** Defaults to "trunk".
599 **
 
 
 
 
 
 
 
600 */
601 void tarball_page(void){
602 int rid;
603 char *zName, *zRid, *zKey;
604 int nName, nRid;
605 Blob tarball;
 
 
 
 
 
606
607 login_check_credentials();
608 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
609 load_control();
610 zName = mprintf("%s", PD("name",""));
611 nName = strlen(zName);
612 zRid = mprintf("%s", PD("uuid","trunk"));
613 nRid = strlen(zRid);
 
 
 
 
 
614 if( nName>7 && fossil_strcmp(&zName[nName-7], ".tar.gz")==0 ){
615 /* Special case: Remove the ".tar.gz" suffix. */
616 nName -= 7;
617 zName[nName] = 0;
618 }else{
@@ -629,16 +676,29 @@
629 if( rid==0 ){
630 @ Not found
631 return;
632 }
633 if( nRid==0 && nName>10 ) zName[10] = 0;
634 zKey = db_text(0, "SELECT '/tarball/'||uuid||'/%q'"
635 " FROM blob WHERE rid=%d",zName,rid);
 
 
 
 
 
 
 
636 if( P("debug")!=0 ){
637 style_header("Tarball Generator Debug Screen");
638 @ zName = "%h(zName)"<br>
639 @ rid = %d(rid)<br>
 
 
 
 
 
 
640 @ zKey = "%h(zKey)"
641 style_footer();
642 return;
643 }
644 if( referred_from_login() ){
@@ -652,14 +712,16 @@
652 style_footer();
653 return;
654 }
655 blob_zero(&tarball);
656 if( cache_read(&tarball, zKey)==0 ){
657 tarball_of_checkin(rid, &tarball, zName);
658 cache_write(&tarball, zKey);
659 }
660 free( zName );
661 free( zRid );
662 free( zKey );
 
 
663 cgi_set_content(&tarball);
664 cgi_set_content_type("application/x-compressed");
665 }
666
--- src/tar.c
+++ src/tar.c
@@ -73,11 +73,11 @@
73 );
74 }
75
76
77 /*
78 ** Verify that all characters in 'zName' are in the
79 ** ISO646 (=ASCII) character set.
80 */
81 static int is_iso646_name(
82 const char *zName, /* file path */
83 int nName /* path length */
@@ -90,11 +90,11 @@
90 return 1;
91 }
92
93
94 /*
95 ** copy string pSrc into pDst, truncating or padding with 0 if necessary
96 */
97 static void padded_copy(
98 char *pDest,
99 int nDest,
100 const char *pSrc,
@@ -446,15 +446,16 @@
446 blob_write_to_file(&zip, g.argv[2]);
447 }
448
449 /*
450 ** Given the RID for a check-in, construct a tarball containing
451 ** all files in that check-in that match pGlob (or all files if
452 ** pGlob is NULL).
453 **
454 ** If RID is for an object that is not a real manifest, then the
455 ** resulting tarball contains a single file which is the RID
456 ** object. pInclude and pExclude are ignored in this case.
457 **
458 ** If the RID object does not exist in the repository, then
459 ** pTar is zeroed.
460 **
461 ** zDir is a "synthetic" subdirectory which all files get
@@ -462,11 +463,17 @@
463 ** which case it is ignored. The intention is to create a tarball which
464 ** politely expands into a subdir instead of filling your current dir
465 ** with source files. For example, pass a UUID or "ProjectName".
466 **
467 */
468 void tarball_of_checkin(
469 int rid, /* The RID of the checkin from which to form a tarball */
470 Blob *pTar, /* Write the tarball into this blob */
471 const char *zDir, /* Directory prefix for all file added to tarball */
472 Glob *pInclude, /* Only add files matching this pattern */
473 Glob *pExclude /* Exclude files matching this pattern */
474 ){
475 Blob mfile, hash, file;
476 Manifest *pManifest;
477 ManifestFile *pFile;
478 Blob filename;
479 int nPrefix;
@@ -488,11 +495,14 @@
495
496 pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
497 if( pManifest ){
498 mTime = (pManifest->rDate - 2440587.5)*86400.0;
499 tar_begin(mTime);
500 if( (pInclude==0 || glob_match(pInclude, "manifest"))
501 && !glob_match(pExclude, "manifest")
502 && db_get_boolean("manifest", 0)
503 ){
504 blob_append(&filename, "manifest", -1);
505 zName = blob_str(&filename);
506 sha1sum_blob(&mfile, &hash);
507 sterilize_manifest(&mfile);
508 tar_add_file(zName, &mfile, 0, mTime);
@@ -504,11 +514,14 @@
514 tar_add_file(zName, &hash, 0, mTime);
515 blob_reset(&hash);
516 }
517 manifest_file_rewind(pManifest);
518 while( (pFile = manifest_file_next(pManifest,0))!=0 ){
519 int fid;
520 if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue;
521 if( glob_match(pExclude, pFile->zName) ) continue;
522 fid = uuid_to_rid(pFile->zUuid, 0);
523 if( fid ){
524 content_get(fid, &file);
525 blob_resize(&filename, nPrefix);
526 blob_append(&filename, pFile->zName, -1);
527 zName = blob_str(&filename);
@@ -539,19 +552,34 @@
552 ** option is used, its argument becomes the name of the top-level directory
553 ** in the resulting tarball. If --name is omitted, the top-level directory
554 ** named is derived from the project name, the check-in date and time, and
555 ** the artifact ID of the check-in.
556 **
557 ** The GLOBLIST argument to --exclude and --include can be a comma-separated
558 ** list of glob patterns, where each glob pattern may optionally be enclosed
559 ** in "..." or '...' so that it may contain commas. If a file matches both
560 ** --include and --exclude then it is excluded.
561 **
562 ** Options:
563 ** -X|--exclude GLOBLIST Comma-separated list of GLOBs of files to exclude
564 ** --include GLOBLIST Comma-separated list of GLOBs of files to include
565 ** --name DIRECTORYNAME The name of the top-level directory in the archive
566 ** -R REPOSITORY Specify a Fossil repository
567 */
568 void tarball_cmd(void){
569 int rid;
570 Blob tarball;
571 const char *zName;
572 Glob *pInclude = 0;
573 Glob *pExclude = 0;
574 const char *zInclude;
575 const char *zExclude;
576 zName = find_option("name", 0, 1);
577 zExclude = find_option("exclude", "X", 1);
578 if( zExclude ) pExclude = glob_create(zExclude);
579 zInclude = find_option("include", 0, 1);
580 if( zInclude ) pInclude = glob_create(zInclude);
581 db_find_and_open_repository(0, 0);
582
583 /* We should be done with options.. */
584 verify_all_options();
585
@@ -573,46 +601,65 @@
601 " WHERE event.objid=%d"
602 " AND blob.rid=%d",
603 db_get("project-name", "unnamed"), rid, rid
604 );
605 }
606 tarball_of_checkin(rid, &tarball, zName, pInclude, pExclude);
607 glob_free(pInclude);
608 glob_free(pExclude);
609 blob_write_to_file(&tarball, g.argv[3]);
610 blob_reset(&tarball);
611 }
612
613 /*
614 ** WEBPAGE: tarball
615 ** URL: /tarball
616 **
617 ** Generate a compressed tarball for a check-in and return that
618 ** tarball as the HTTP reply content.
619 **
620 ** Query parameters:
621 **
622 ** name=NAME[.tar.gz] The base name of the output file. The default
623 ** value is a configuration parameter in the project
624 ** settings. A prefix of the name, omitting the extension,
625 ** is used as the top-most directory name.
626 **
627 ** uuid=TAG The check-in that is turned into a tarball.
628 ** Defaults to "trunk".
629 **
630 ** in=PATTERN Only include files that match the comma-separate
631 ** list of GLOB patterns in PATTERN, as with ex=
632 **
633 ** ex=PATTERN Omit any file that match PATTERN. PATTERN is a
634 ** comma-separated list of GLOB patterns, where each
635 ** pattern can optionally be quoted using ".." or '..'.
636 ** Any file matching both ex= and in= is excluded.
637 */
638 void tarball_page(void){
639 int rid;
640 char *zName, *zRid, *zKey;
641 int nName, nRid;
642 const char *zInclude; /* The in= query parameter */
643 const char *zExclude; /* The ex= query parameter */
644 Blob cacheKey; /* The key to cache */
645 Glob *pInclude = 0; /* The compiled in= glob pattern */
646 Glob *pExclude = 0; /* The compiled ex= glob pattern */
647 Blob tarball; /* Tarball accumulated here */
648
649 login_check_credentials();
650 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
651 load_control();
652 zName = mprintf("%s", PD("name",""));
653 nName = strlen(zName);
654 zRid = mprintf("%s", PD("uuid","trunk"));
655 nRid = strlen(zRid);
656 zInclude = P("in");
657 if( zInclude ) pInclude = glob_create(zInclude);
658 zExclude = P("ex");
659 if( zExclude ) pExclude = glob_create(zExclude);
660
661 if( nName>7 && fossil_strcmp(&zName[nName-7], ".tar.gz")==0 ){
662 /* Special case: Remove the ".tar.gz" suffix. */
663 nName -= 7;
664 zName[nName] = 0;
665 }else{
@@ -629,16 +676,29 @@
676 if( rid==0 ){
677 @ Not found
678 return;
679 }
680 if( nRid==0 && nName>10 ) zName[10] = 0;
681
682 /* Compute a unique key for the cache entry based on query parameters */
683 blob_init(&cacheKey, 0, 0);
684 blob_appendf(&cacheKey, "/tarball/%z", rid_to_uuid(rid));
685 if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude);
686 if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude);
687 blob_appendf(&cacheKey, "/%q", zName);
688 zKey = blob_str(&cacheKey);
689
690 if( P("debug")!=0 ){
691 style_header("Tarball Generator Debug Screen");
692 @ zName = "%h(zName)"<br>
693 @ rid = %d(rid)<br>
694 if( zInclude ){
695 @ zInclude = "%h(zInclude)"<br>
696 }
697 if( zExclude ){
698 @ zExclude = "%h(zExclude)"<br>
699 }
700 @ zKey = "%h(zKey)"
701 style_footer();
702 return;
703 }
704 if( referred_from_login() ){
@@ -652,14 +712,16 @@
712 style_footer();
713 return;
714 }
715 blob_zero(&tarball);
716 if( cache_read(&tarball, zKey)==0 ){
717 tarball_of_checkin(rid, &tarball, zName, pInclude, pExclude);
718 cache_write(&tarball, zKey);
719 }
720 glob_free(pInclude);
721 glob_free(pExclude);
722 fossil_free(zName);
723 fossil_free(zRid);
724 blob_reset(&cacheKey);
725 cgi_set_content(&tarball);
726 cgi_set_content_type("application/x-compressed");
727 }
728
+83 -23
--- src/zip.c
+++ src/zip.c
@@ -309,11 +309,11 @@
309309
** Given the RID for a manifest, construct a ZIP archive containing
310310
** all files in the corresponding baseline.
311311
**
312312
** If RID is for an object that is not a real manifest, then the
313313
** resulting ZIP archive contains a single file which is the RID
314
-** object.
314
+** object. The pInclude and pExclude parameters are ignored in this case.
315315
**
316316
** If the RID object does not exist in the repository, then
317317
** pZip is zeroed.
318318
**
319319
** zDir is a "synthetic" subdirectory which all zipped files get
@@ -321,11 +321,17 @@
321321
** in which case it is ignored. The intention is to create a zip which
322322
** politely expands into a subdir instead of filling your current dir
323323
** with source files. For example, pass a UUID or "ProjectName".
324324
**
325325
*/
326
-void zip_of_baseline(int rid, Blob *pZip, const char *zDir){
326
+void zip_of_checkin(
327
+ int rid, /* The RID of the checkin to construct the ZIP archive from */
328
+ Blob *pZip, /* Write the ZIP archive content into this blob */
329
+ const char *zDir, /* Top-level directory of the ZIP archive */
330
+ Glob *pInclude, /* Only include files that match this pattern */
331
+ Glob *pExclude /* Exclude files that match this pattern */
332
+){
327333
Blob mfile, hash, file;
328334
Manifest *pManifest;
329335
ManifestFile *pFile;
330336
Blob filename;
331337
int nPrefix;
@@ -346,11 +352,14 @@
346352
347353
pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
348354
if( pManifest ){
349355
char *zName;
350356
zip_set_timedate(pManifest->rDate);
351
- if( db_get_boolean("manifest", 0) ){
357
+ if( (pInclude==0 || glob_match(pInclude, "manifest"))
358
+ && !glob_match(pExclude, "manifest")
359
+ && db_get_boolean("manifest", 0)
360
+ ){
352361
blob_append(&filename, "manifest", -1);
353362
zName = blob_str(&filename);
354363
zip_add_folders(zName);
355364
sha1sum_blob(&mfile, &hash);
356365
sterilize_manifest(&mfile);
@@ -363,11 +372,14 @@
363372
zip_add_file(zName, &hash, 0);
364373
blob_reset(&hash);
365374
}
366375
manifest_file_rewind(pManifest);
367376
while( (pFile = manifest_file_next(pManifest,0))!=0 ){
368
- int fid = uuid_to_rid(pFile->zUuid, 0);
377
+ int fid;
378
+ if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue;
379
+ if( glob_match(pExclude, pFile->zName) ) continue;
380
+ fid = uuid_to_rid(pFile->zUuid, 0);
369381
if( fid ){
370382
content_get(fid, &file);
371383
blob_resize(&filename, nPrefix);
372384
blob_append(&filename, pFile->zName, -1);
373385
zName = blob_str(&filename);
@@ -385,23 +397,42 @@
385397
}
386398
387399
/*
388400
** COMMAND: zip*
389401
**
390
-** Usage: %fossil zip VERSION OUTPUTFILE [--name DIRECTORYNAME] [-R|--repository REPO]
402
+** Usage: %fossil zip VERSION OUTPUTFILE [OPTIONS]
391403
**
392
-** Generate a ZIP archive for a specified version. If the --name option is
404
+** Generate a ZIP archive for a check-in. If the --name option is
393405
** used, its argument becomes the name of the top-level directory in the
394406
** resulting ZIP archive. If --name is omitted, the top-level directory
395407
** named is derived from the project name, the check-in date and time, and
396408
** the artifact ID of the check-in.
409
+**
410
+** The GLOBLIST argument to --exclude and --include can be a comma-separated
411
+** list of glob patterns, where each glob pattern may optionally be enclosed
412
+** in "..." or '...' so that it may contain commas. If a file matches both
413
+** --include and --exclude then it is excluded.
414
+**
415
+** Options:
416
+** -X|--exclude GLOBLIST Comma-separated list of GLOBs of files to exclude
417
+** --include GLOBLIST Comma-separated list of GLOBs of files to include
418
+** --name DIRECTORYNAME The name of the top-level directory in the archive
419
+** -R REPOSITORY Specify a Fossil repository
397420
*/
398
-void baseline_zip_cmd(void){
421
+void zip_cmd(void){
399422
int rid;
400423
Blob zip;
401424
const char *zName;
425
+ Glob *pInclude = 0;
426
+ Glob *pExclude = 0;
427
+ const char *zInclude;
428
+ const char *zExclude;
402429
zName = find_option("name", 0, 1);
430
+ zExclude = find_option("exclude", "X", 1);
431
+ if( zExclude ) pExclude = glob_create(zExclude);
432
+ zInclude = find_option("include", 0, 1);
433
+ if( zInclude ) pInclude = glob_create(zInclude);
403434
db_find_and_open_repository(0, 0);
404435
405436
/* We should be done with options.. */
406437
verify_all_options();
407438
@@ -418,46 +449,65 @@
418449
" WHERE event.objid=%d"
419450
" AND blob.rid=%d",
420451
db_get("project-name", "unnamed"), rid, rid
421452
);
422453
}
423
- zip_of_baseline(rid, &zip, zName);
454
+ zip_of_checkin(rid, &zip, zName, pInclude, pExclude);
424455
blob_write_to_file(&zip, g.argv[3]);
456
+ glob_free(pInclude);
457
+ glob_free(pExclude);
425458
}
426459
427460
/*
428461
** WEBPAGE: zip
429
-** URL: /zip/RID.zip
462
+** URL: /zip
430463
**
431
-** Generate a ZIP archive for the baseline.
464
+** Generate a ZIP archive for a checkin specified by the uuid= query parameter.
432465
** Return that ZIP archive as the HTTP reply content.
433466
**
434
-** Optional URL Parameters:
435
-**
436
-** - name=NAME[.zip] is the name of the output file. Defaults to
437
-** something project/version-specific. The base part of the
438
-** name, up to the last dot, is used as the top-most directory
439
-** name in the output file.
440
-**
441
-** - uuid=the version to zip (may be a tag/branch name).
442
-** Defaults to "trunk".
443
-**
467
+** Query parameters:
468
+**
469
+** name=NAME[.tar.gz] The base name of the output file. The default
470
+** value is a configuration parameter in the project
471
+** settings. A prefix of the name, omitting the extension,
472
+** is used as the top-most directory name.
473
+
474
+**
475
+** uuid=TAG The check-in that is turned into a tarball.
476
+** Defaults to "trunk".
477
+**
478
+** in=PATTERN Only include files that match the comma-separate
479
+** list of GLOB patterns in PATTERN, as with ex=
480
+**
481
+** ex=PATTERN Omit any file that match PATTERN. PATTERN is a
482
+** comma-separated list of GLOB patterns, where each
483
+** pattern can optionally be quoted using ".." or '..'.
484
+** Any file matching both ex= and in= is excluded.
444485
*/
445486
void baseline_zip_page(void){
446487
int rid;
447488
char *zName, *zRid;
448489
int nName, nRid;
449490
Blob zip;
450491
char *zKey;
492
+ const char *zInclude; /* The in= query parameter */
493
+ const char *zExclude; /* The ex= query parameter */
494
+ Blob cacheKey; /* The key to cache */
495
+ Glob *pInclude = 0; /* The compiled in= glob pattern */
496
+ Glob *pExclude = 0; /* The compiled ex= glob pattern */
451497
452498
login_check_credentials();
453499
if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
454500
load_control();
455501
zName = mprintf("%s", PD("name",""));
456502
nName = strlen(zName);
457503
zRid = mprintf("%s", PD("uuid","trunk"));
458504
nRid = strlen(zRid);
505
+ zInclude = P("in");
506
+ if( zInclude ) pInclude = glob_create(zInclude);
507
+ zExclude = P("ex");
508
+ if( zExclude ) pExclude = glob_create(zExclude);
459509
if( nName>4 && fossil_strcmp(&zName[nName-4], ".zip")==0 ){
460510
/* Special case: Remove the ".zip" suffix. */
461511
nName -= 4;
462512
zName[nName] = 0;
463513
}else{
@@ -485,17 +535,27 @@
485535
@ </form>
486536
style_footer();
487537
return;
488538
}
489539
if( nRid==0 && nName>10 ) zName[10] = 0;
490
- zKey = db_text(0, "SELECT '/zip/'||uuid||'/%q' FROM blob WHERE rid=%d",zName,rid);
540
+
541
+ /* Compute a unique key for the cache entry based on query parameters */
542
+ blob_init(&cacheKey, 0, 0);
543
+ blob_appendf(&cacheKey, "/zip/%z", rid_to_uuid(rid));
544
+ if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude);
545
+ if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude);
546
+ blob_appendf(&cacheKey, "/%q", zName);
547
+ zKey = blob_str(&cacheKey);
548
+
491549
blob_zero(&zip);
492550
if( cache_read(&zip, zKey)==0 ){
493
- zip_of_baseline(rid, &zip, zName);
551
+ zip_of_checkin(rid, &zip, zName, pInclude, pExclude);
494552
cache_write(&zip, zKey);
495553
}
496554
fossil_free( zName );
497555
fossil_free( zRid );
498
- fossil_free( zKey );
556
+ blob_reset(&cacheKey);
557
+ glob_free(pInclude);
558
+ glob_free(pExclude);
499559
cgi_set_content(&zip);
500560
cgi_set_content_type("application/zip");
501561
}
502562
--- src/zip.c
+++ src/zip.c
@@ -309,11 +309,11 @@
309 ** Given the RID for a manifest, construct a ZIP archive containing
310 ** all files in the corresponding baseline.
311 **
312 ** If RID is for an object that is not a real manifest, then the
313 ** resulting ZIP archive contains a single file which is the RID
314 ** object.
315 **
316 ** If the RID object does not exist in the repository, then
317 ** pZip is zeroed.
318 **
319 ** zDir is a "synthetic" subdirectory which all zipped files get
@@ -321,11 +321,17 @@
321 ** in which case it is ignored. The intention is to create a zip which
322 ** politely expands into a subdir instead of filling your current dir
323 ** with source files. For example, pass a UUID or "ProjectName".
324 **
325 */
326 void zip_of_baseline(int rid, Blob *pZip, const char *zDir){
 
 
 
 
 
 
327 Blob mfile, hash, file;
328 Manifest *pManifest;
329 ManifestFile *pFile;
330 Blob filename;
331 int nPrefix;
@@ -346,11 +352,14 @@
346
347 pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
348 if( pManifest ){
349 char *zName;
350 zip_set_timedate(pManifest->rDate);
351 if( db_get_boolean("manifest", 0) ){
 
 
 
352 blob_append(&filename, "manifest", -1);
353 zName = blob_str(&filename);
354 zip_add_folders(zName);
355 sha1sum_blob(&mfile, &hash);
356 sterilize_manifest(&mfile);
@@ -363,11 +372,14 @@
363 zip_add_file(zName, &hash, 0);
364 blob_reset(&hash);
365 }
366 manifest_file_rewind(pManifest);
367 while( (pFile = manifest_file_next(pManifest,0))!=0 ){
368 int fid = uuid_to_rid(pFile->zUuid, 0);
 
 
 
369 if( fid ){
370 content_get(fid, &file);
371 blob_resize(&filename, nPrefix);
372 blob_append(&filename, pFile->zName, -1);
373 zName = blob_str(&filename);
@@ -385,23 +397,42 @@
385 }
386
387 /*
388 ** COMMAND: zip*
389 **
390 ** Usage: %fossil zip VERSION OUTPUTFILE [--name DIRECTORYNAME] [-R|--repository REPO]
391 **
392 ** Generate a ZIP archive for a specified version. If the --name option is
393 ** used, its argument becomes the name of the top-level directory in the
394 ** resulting ZIP archive. If --name is omitted, the top-level directory
395 ** named is derived from the project name, the check-in date and time, and
396 ** the artifact ID of the check-in.
 
 
 
 
 
 
 
 
 
 
 
397 */
398 void baseline_zip_cmd(void){
399 int rid;
400 Blob zip;
401 const char *zName;
 
 
 
 
402 zName = find_option("name", 0, 1);
 
 
 
 
403 db_find_and_open_repository(0, 0);
404
405 /* We should be done with options.. */
406 verify_all_options();
407
@@ -418,46 +449,65 @@
418 " WHERE event.objid=%d"
419 " AND blob.rid=%d",
420 db_get("project-name", "unnamed"), rid, rid
421 );
422 }
423 zip_of_baseline(rid, &zip, zName);
424 blob_write_to_file(&zip, g.argv[3]);
 
 
425 }
426
427 /*
428 ** WEBPAGE: zip
429 ** URL: /zip/RID.zip
430 **
431 ** Generate a ZIP archive for the baseline.
432 ** Return that ZIP archive as the HTTP reply content.
433 **
434 ** Optional URL Parameters:
435 **
436 ** - name=NAME[.zip] is the name of the output file. Defaults to
437 ** something project/version-specific. The base part of the
438 ** name, up to the last dot, is used as the top-most directory
439 ** name in the output file.
440 **
441 ** - uuid=the version to zip (may be a tag/branch name).
442 ** Defaults to "trunk".
443 **
 
 
 
 
 
 
 
 
444 */
445 void baseline_zip_page(void){
446 int rid;
447 char *zName, *zRid;
448 int nName, nRid;
449 Blob zip;
450 char *zKey;
 
 
 
 
 
451
452 login_check_credentials();
453 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
454 load_control();
455 zName = mprintf("%s", PD("name",""));
456 nName = strlen(zName);
457 zRid = mprintf("%s", PD("uuid","trunk"));
458 nRid = strlen(zRid);
 
 
 
 
459 if( nName>4 && fossil_strcmp(&zName[nName-4], ".zip")==0 ){
460 /* Special case: Remove the ".zip" suffix. */
461 nName -= 4;
462 zName[nName] = 0;
463 }else{
@@ -485,17 +535,27 @@
485 @ </form>
486 style_footer();
487 return;
488 }
489 if( nRid==0 && nName>10 ) zName[10] = 0;
490 zKey = db_text(0, "SELECT '/zip/'||uuid||'/%q' FROM blob WHERE rid=%d",zName,rid);
 
 
 
 
 
 
 
 
491 blob_zero(&zip);
492 if( cache_read(&zip, zKey)==0 ){
493 zip_of_baseline(rid, &zip, zName);
494 cache_write(&zip, zKey);
495 }
496 fossil_free( zName );
497 fossil_free( zRid );
498 fossil_free( zKey );
 
 
499 cgi_set_content(&zip);
500 cgi_set_content_type("application/zip");
501 }
502
--- src/zip.c
+++ src/zip.c
@@ -309,11 +309,11 @@
309 ** Given the RID for a manifest, construct a ZIP archive containing
310 ** all files in the corresponding baseline.
311 **
312 ** If RID is for an object that is not a real manifest, then the
313 ** resulting ZIP archive contains a single file which is the RID
314 ** object. The pInclude and pExclude parameters are ignored in this case.
315 **
316 ** If the RID object does not exist in the repository, then
317 ** pZip is zeroed.
318 **
319 ** zDir is a "synthetic" subdirectory which all zipped files get
@@ -321,11 +321,17 @@
321 ** in which case it is ignored. The intention is to create a zip which
322 ** politely expands into a subdir instead of filling your current dir
323 ** with source files. For example, pass a UUID or "ProjectName".
324 **
325 */
326 void zip_of_checkin(
327 int rid, /* The RID of the checkin to construct the ZIP archive from */
328 Blob *pZip, /* Write the ZIP archive content into this blob */
329 const char *zDir, /* Top-level directory of the ZIP archive */
330 Glob *pInclude, /* Only include files that match this pattern */
331 Glob *pExclude /* Exclude files that match this pattern */
332 ){
333 Blob mfile, hash, file;
334 Manifest *pManifest;
335 ManifestFile *pFile;
336 Blob filename;
337 int nPrefix;
@@ -346,11 +352,14 @@
352
353 pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0);
354 if( pManifest ){
355 char *zName;
356 zip_set_timedate(pManifest->rDate);
357 if( (pInclude==0 || glob_match(pInclude, "manifest"))
358 && !glob_match(pExclude, "manifest")
359 && db_get_boolean("manifest", 0)
360 ){
361 blob_append(&filename, "manifest", -1);
362 zName = blob_str(&filename);
363 zip_add_folders(zName);
364 sha1sum_blob(&mfile, &hash);
365 sterilize_manifest(&mfile);
@@ -363,11 +372,14 @@
372 zip_add_file(zName, &hash, 0);
373 blob_reset(&hash);
374 }
375 manifest_file_rewind(pManifest);
376 while( (pFile = manifest_file_next(pManifest,0))!=0 ){
377 int fid;
378 if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue;
379 if( glob_match(pExclude, pFile->zName) ) continue;
380 fid = uuid_to_rid(pFile->zUuid, 0);
381 if( fid ){
382 content_get(fid, &file);
383 blob_resize(&filename, nPrefix);
384 blob_append(&filename, pFile->zName, -1);
385 zName = blob_str(&filename);
@@ -385,23 +397,42 @@
397 }
398
399 /*
400 ** COMMAND: zip*
401 **
402 ** Usage: %fossil zip VERSION OUTPUTFILE [OPTIONS]
403 **
404 ** Generate a ZIP archive for a check-in. If the --name option is
405 ** used, its argument becomes the name of the top-level directory in the
406 ** resulting ZIP archive. If --name is omitted, the top-level directory
407 ** named is derived from the project name, the check-in date and time, and
408 ** the artifact ID of the check-in.
409 **
410 ** The GLOBLIST argument to --exclude and --include can be a comma-separated
411 ** list of glob patterns, where each glob pattern may optionally be enclosed
412 ** in "..." or '...' so that it may contain commas. If a file matches both
413 ** --include and --exclude then it is excluded.
414 **
415 ** Options:
416 ** -X|--exclude GLOBLIST Comma-separated list of GLOBs of files to exclude
417 ** --include GLOBLIST Comma-separated list of GLOBs of files to include
418 ** --name DIRECTORYNAME The name of the top-level directory in the archive
419 ** -R REPOSITORY Specify a Fossil repository
420 */
421 void zip_cmd(void){
422 int rid;
423 Blob zip;
424 const char *zName;
425 Glob *pInclude = 0;
426 Glob *pExclude = 0;
427 const char *zInclude;
428 const char *zExclude;
429 zName = find_option("name", 0, 1);
430 zExclude = find_option("exclude", "X", 1);
431 if( zExclude ) pExclude = glob_create(zExclude);
432 zInclude = find_option("include", 0, 1);
433 if( zInclude ) pInclude = glob_create(zInclude);
434 db_find_and_open_repository(0, 0);
435
436 /* We should be done with options.. */
437 verify_all_options();
438
@@ -418,46 +449,65 @@
449 " WHERE event.objid=%d"
450 " AND blob.rid=%d",
451 db_get("project-name", "unnamed"), rid, rid
452 );
453 }
454 zip_of_checkin(rid, &zip, zName, pInclude, pExclude);
455 blob_write_to_file(&zip, g.argv[3]);
456 glob_free(pInclude);
457 glob_free(pExclude);
458 }
459
460 /*
461 ** WEBPAGE: zip
462 ** URL: /zip
463 **
464 ** Generate a ZIP archive for a checkin specified by the uuid= query parameter.
465 ** Return that ZIP archive as the HTTP reply content.
466 **
467 ** Query parameters:
468 **
469 ** name=NAME[.tar.gz] The base name of the output file. The default
470 ** value is a configuration parameter in the project
471 ** settings. A prefix of the name, omitting the extension,
472 ** is used as the top-most directory name.
473
474 **
475 ** uuid=TAG The check-in that is turned into a tarball.
476 ** Defaults to "trunk".
477 **
478 ** in=PATTERN Only include files that match the comma-separate
479 ** list of GLOB patterns in PATTERN, as with ex=
480 **
481 ** ex=PATTERN Omit any file that match PATTERN. PATTERN is a
482 ** comma-separated list of GLOB patterns, where each
483 ** pattern can optionally be quoted using ".." or '..'.
484 ** Any file matching both ex= and in= is excluded.
485 */
486 void baseline_zip_page(void){
487 int rid;
488 char *zName, *zRid;
489 int nName, nRid;
490 Blob zip;
491 char *zKey;
492 const char *zInclude; /* The in= query parameter */
493 const char *zExclude; /* The ex= query parameter */
494 Blob cacheKey; /* The key to cache */
495 Glob *pInclude = 0; /* The compiled in= glob pattern */
496 Glob *pExclude = 0; /* The compiled ex= glob pattern */
497
498 login_check_credentials();
499 if( !g.perm.Zip ){ login_needed(g.anon.Zip); return; }
500 load_control();
501 zName = mprintf("%s", PD("name",""));
502 nName = strlen(zName);
503 zRid = mprintf("%s", PD("uuid","trunk"));
504 nRid = strlen(zRid);
505 zInclude = P("in");
506 if( zInclude ) pInclude = glob_create(zInclude);
507 zExclude = P("ex");
508 if( zExclude ) pExclude = glob_create(zExclude);
509 if( nName>4 && fossil_strcmp(&zName[nName-4], ".zip")==0 ){
510 /* Special case: Remove the ".zip" suffix. */
511 nName -= 4;
512 zName[nName] = 0;
513 }else{
@@ -485,17 +535,27 @@
535 @ </form>
536 style_footer();
537 return;
538 }
539 if( nRid==0 && nName>10 ) zName[10] = 0;
540
541 /* Compute a unique key for the cache entry based on query parameters */
542 blob_init(&cacheKey, 0, 0);
543 blob_appendf(&cacheKey, "/zip/%z", rid_to_uuid(rid));
544 if( zInclude ) blob_appendf(&cacheKey, ",in=%Q", zInclude);
545 if( zExclude ) blob_appendf(&cacheKey, ",ex=%Q", zExclude);
546 blob_appendf(&cacheKey, "/%q", zName);
547 zKey = blob_str(&cacheKey);
548
549 blob_zero(&zip);
550 if( cache_read(&zip, zKey)==0 ){
551 zip_of_checkin(rid, &zip, zName, pInclude, pExclude);
552 cache_write(&zip, zKey);
553 }
554 fossil_free( zName );
555 fossil_free( zRid );
556 blob_reset(&cacheKey);
557 glob_free(pInclude);
558 glob_free(pExclude);
559 cgi_set_content(&zip);
560 cgi_set_content_type("application/zip");
561 }
562

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button