Index: src/add.c ================================================================== --- src/add.c +++ src/add.c @@ -71,29 +71,42 @@ ".fos-journal", ".fos-wal", ".fos-shm", }; - /* Names of auxiliary files generated by SQLite when the "manifest" - ** property is enabled + /* Possible names of auxiliary files generated when the "manifest" property + ** is used */ - static const char *const azManifest[] = { - "manifest", - "manifest.uuid", + static const struct { + const char *fname; + int flg; + }aManifestflags[] = { + { "manifest", MFESTFLG_RAW }, + { "manifest.uuid", MFESTFLG_UUID }, + { "manifest.tags", MFESTFLG_TAGS } }; + static const char *azManifests[3]; /* ** Names of repository files, if they exist in the checkout. */ static const char *azRepo[4] = { 0, 0, 0, 0 }; /* Cached setting "manifest" */ static int cachedManifest = -1; + static int numManifests; if( cachedManifest == -1 ){ + int i; Blob repo; - cachedManifest = db_get_boolean("manifest",0); + cachedManifest = db_get_manifest_setting(); + numManifests = 0; + for(i=0; i1 ){ fossil_print("**** warning: a fork has occurred *****\n"); } } Index: src/checkout.c ================================================================== --- src/checkout.c +++ src/checkout.c @@ -127,44 +127,102 @@ /* ** If the "manifest" setting is true, then automatically generate ** files named "manifest" and "manifest.uuid" containing, respectively, ** the text of the manifest and the artifact ID of the manifest. +** If the manifest setting is set, but is not a boolean value, then treat +** each character as a flag to enable writing "manifest", "manifest.uuid" or +** "manifest.tags". */ void manifest_to_disk(int vid){ char *zManFile; Blob manifest; Blob hash; + Blob taglist; + int flg; + + flg = db_get_manifest_setting(); - if( db_get_boolean("manifest",0) ){ + if( flg & (MFESTFLG_RAW|MFESTFLG_UUID) ){ blob_zero(&manifest); content_get(vid, &manifest); - zManFile = mprintf("%smanifest", g.zLocalRoot); blob_zero(&hash); sha1sum_blob(&manifest, &hash); sterilize_manifest(&manifest); + } + if( flg & MFESTFLG_RAW ){ + zManFile = mprintf("%smanifest", g.zLocalRoot); blob_write_to_file(&manifest, zManFile); free(zManFile); - zManFile = mprintf("%smanifest.uuid", g.zLocalRoot); - blob_append(&hash, "\n", 1); - blob_write_to_file(&hash, zManFile); - free(zManFile); - blob_reset(&hash); }else{ if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest'") ){ zManFile = mprintf("%smanifest", g.zLocalRoot); file_delete(zManFile); free(zManFile); } + } + if( flg & MFESTFLG_UUID ){ + zManFile = mprintf("%smanifest.uuid", g.zLocalRoot); + blob_append(&hash, "\n", 1); + blob_write_to_file(&hash, zManFile); + free(zManFile); + blob_reset(&hash); + }else{ if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest.uuid'") ){ zManFile = mprintf("%smanifest.uuid", g.zLocalRoot); file_delete(zManFile); free(zManFile); } } + if( flg & MFESTFLG_TAGS ){ + blob_zero(&taglist); + zManFile = mprintf("%smanifest.tags", g.zLocalRoot); + get_checkin_taglist(vid, &taglist); + blob_write_to_file(&taglist, zManFile); + free(zManFile); + blob_reset(&taglist); + }else{ + if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest.tags'") ){ + zManFile = mprintf("%smanifest.tags", g.zLocalRoot); + file_delete(zManFile); + free(zManFile); + } + } +} +/* +** Find the branch name and all symbolic tags for a particular check-in +** identified by "rid". +** +** The branch name is actually only extracted if this procedure is run +** from within a local check-out. And the branch name is not the branch +** name for "rid" but rather the branch name for the current check-out. +** It is unclear if the rid parameter is always the same as the current +** check-out. +*/ +void get_checkin_taglist(int rid, Blob *pOut){ + Stmt stmt; + char *zCurrent; + blob_reset(pOut); + zCurrent = db_text(0, "SELECT value FROM tagxref" + " WHERE rid=%d AND tagid=%d", rid, TAG_BRANCH); + blob_appendf(pOut, "branch %s\n", zCurrent); + db_prepare(&stmt, "SELECT substr(tagname, 5)" + " FROM tagxref, tag" + " WHERE tagxref.rid=%d" + " AND tagxref.tagtype>0" + " AND tag.tagid=tagxref.tagid" + " AND tag.tagname GLOB 'sym-*'", rid); + while( db_step(&stmt)==SQLITE_ROW ){ + const char *zName; + zName = db_column_text(&stmt, 0); + blob_appendf(pOut, "tag %s\n", zName); + } + db_reset(&stmt); + db_finalize(&stmt); } + /* ** COMMAND: checkout* ** COMMAND: co* ** Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -2270,10 +2270,46 @@ return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName); } void db_lset_int(const char *zName, int value){ db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value); } + +#if INTERFACE +/* Manifest generation flags */ +#define MFESTFLG_RAW 0x01 +#define MFESTFLG_UUID 0x02 +#define MFESTFLG_TAGS 0x04 +#endif /* INTERFACE */ + +/* +** Get the manifest setting. For backwards compatibility first check if the +** value is a boolean. If it's not a boolean, treat each character as a flag +** to enable a manifest type. This system puts certain boundary conditions on +** which letters can be used to represent flags (any permutation of flags must +** not be able to fully form one of the boolean values). +*/ +int db_get_manifest_setting(void){ + int flg; + char *zNVVal = db_get("manifest", "off"); + char *zVal = db_get_versioned("manifest", zNVVal); + if( is_false(zVal) ){ + return 0; + }else if( is_truth(zVal) ) { + return MFESTFLG_RAW|MFESTFLG_UUID; + } + flg = 0; + while( *zVal ){ + switch( *zVal ){ + case 'r': flg |= MFESTFLG_RAW; break; + case 'u': flg |= MFESTFLG_UUID; break; + case 't': flg |= MFESTFLG_TAGS; break; + } + zVal++; + } + return flg; +} + /* ** Record the name of a local repository in the global_config() database. ** The repository filename %s is recorded as an entry with a "name" field ** of the following form: @@ -2570,11 +2606,11 @@ { "https-login", 0, 0, 0, 0, "off" }, { "ignore-glob", 0, 40, 1, 0, "" }, { "keep-glob", 0, 40, 1, 0, "" }, { "localauth", 0, 0, 0, 0, "off" }, { "main-branch", 0, 40, 0, 0, "trunk" }, - { "manifest", 0, 0, 1, 0, "off" }, + { "manifest", 0, 5, 0, 0, "off" }, { "max-loadavg", 0, 25, 0, 0, "0.0" }, { "max-upload", 0, 25, 0, 0, "250000" }, { "mtime-changes", 0, 0, 0, 0, "on" }, #if FOSSIL_ENABLE_LEGACY_MV_RM { "mv-rm-files", 0, 0, 0, 0, "off" }, @@ -2775,13 +2811,16 @@ ** false, all HTTP requests from localhost have ** unrestricted access to the repository. ** ** main-branch The primary branch for the project. Default: trunk ** -** manifest If enabled, automatically create files "manifest" and -** (versionable) "manifest.uuid" in every checkout. The SQLite and -** Fossil repositories both require this. Default: off. +** manifest If set to a true boolean value, automatically create +** (versionable) files "manifest" and "manifest.uuid" in every checkout. +** Optionally use combinations of characters 'r' +** for "manifest", 'u' for "manifest.uuid" and 't' for +** "manifest.tags". The SQLite and Fossil repositories +** both require manifests. Default: off. ** ** max-loadavg Some CPU-intensive web pages (ex: /zip, /tarball, /blame) ** are disallowed if the system load average goes above this ** value. "0.0" means no limit. This only works on unix. ** Only local settings of this value make a difference since Index: src/tag.c ================================================================== --- src/tag.c +++ src/tag.c @@ -359,10 +359,11 @@ }else{ nrid = content_put(&ctrl); manifest_crosslink(nrid, &ctrl, MC_PERMIT_HOOKS); } assert( blob_is_reset(&ctrl) ); + manifest_to_disk(rid); } /* ** COMMAND: tag ** Index: src/tar.c ================================================================== --- src/tar.c +++ src/tar.c @@ -493,28 +493,64 @@ } nPrefix = blob_size(&filename); pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0); if( pManifest ){ + int flg, eflg = 0; mTime = (pManifest->rDate - 2440587.5)*86400.0; tar_begin(mTime); - if( (pInclude==0 || glob_match(pInclude, "manifest")) - && !glob_match(pExclude, "manifest") - && db_get_boolean("manifest", 0) - ){ - blob_append(&filename, "manifest", -1); - zName = blob_str(&filename); - sha1sum_blob(&mfile, &hash); - sterilize_manifest(&mfile); - tar_add_file(zName, &mfile, 0, mTime); + flg = db_get_manifest_setting(); + if( flg ){ + /* eflg is the effective flags, taking include/exclude into account */ + if( (pInclude==0 || glob_match(pInclude, "manifest")) + && !glob_match(pExclude, "manifest") + && (flg & MFESTFLG_RAW) ){ + eflg |= MFESTFLG_RAW; + } + if( (pInclude==0 || glob_match(pInclude, "manifest.uuid")) + && !glob_match(pExclude, "manifest.uuid") + && (flg & MFESTFLG_UUID) ){ + eflg |= MFESTFLG_UUID; + } + if( (pInclude==0 || glob_match(pInclude, "manifest.tags")) + && !glob_match(pExclude, "manifest.tags") + && (flg & MFESTFLG_TAGS) ){ + eflg |= MFESTFLG_TAGS; + } + + if( eflg & (MFESTFLG_RAW|MFESTFLG_UUID) ){ + if( eflg & MFESTFLG_RAW ){ + blob_append(&filename, "manifest", -1); + zName = blob_str(&filename); + } + if( eflg & MFESTFLG_UUID ){ + sha1sum_blob(&mfile, &hash); + } + if( eflg & MFESTFLG_RAW ) { + sterilize_manifest(&mfile); + tar_add_file(zName, &mfile, 0, mTime); + } + } blob_reset(&mfile); - blob_append(&hash, "\n", 1); - blob_resize(&filename, nPrefix); - blob_append(&filename, "manifest.uuid", -1); - zName = blob_str(&filename); - tar_add_file(zName, &hash, 0, mTime); - blob_reset(&hash); + if( eflg & MFESTFLG_UUID ){ + blob_append(&hash, "\n", 1); + blob_resize(&filename, nPrefix); + blob_append(&filename, "manifest.uuid", -1); + zName = blob_str(&filename); + tar_add_file(zName, &hash, 0, mTime); + blob_reset(&hash); + } + if( eflg & MFESTFLG_TAGS ){ + Blob tagslist; + blob_zero(&tagslist); + get_checkin_taglist(rid, &tagslist); + blob_resize(&filename, nPrefix); + blob_append(&filename, "manifest.tags", -1); + zName = blob_str(&filename); + tar_add_file(zName, &tagslist, 0, mTime); + blob_reset(&tagslist); + } } manifest_file_rewind(pManifest); while( (pFile = manifest_file_next(pManifest,0))!=0 ){ int fid; if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue; Index: src/zip.c ================================================================== --- src/zip.c +++ src/zip.c @@ -350,29 +350,67 @@ } nPrefix = blob_size(&filename); pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0); if( pManifest ){ + int flg, eflg = 0; char *zName; zip_set_timedate(pManifest->rDate); - if( (pInclude==0 || glob_match(pInclude, "manifest")) - && !glob_match(pExclude, "manifest") - && db_get_boolean("manifest", 0) - ){ - blob_append(&filename, "manifest", -1); - zName = blob_str(&filename); - zip_add_folders(zName); - sha1sum_blob(&mfile, &hash); - sterilize_manifest(&mfile); - zip_add_file(zName, &mfile, 0); + flg = db_get_manifest_setting(); + if( flg ){ + /* eflg is the effective flags, taking include/exclude into account */ + if( (pInclude==0 || glob_match(pInclude, "manifest")) + && !glob_match(pExclude, "manifest") + && (flg & MFESTFLG_RAW) ){ + eflg |= MFESTFLG_RAW; + } + if( (pInclude==0 || glob_match(pInclude, "manifest.uuid")) + && !glob_match(pExclude, "manifest.uuid") + && (flg & MFESTFLG_UUID) ){ + eflg |= MFESTFLG_UUID; + } + if( (pInclude==0 || glob_match(pInclude, "manifest.tags")) + && !glob_match(pExclude, "manifest.tags") + && (flg & MFESTFLG_TAGS) ){ + eflg |= MFESTFLG_TAGS; + } + + if( eflg & (MFESTFLG_RAW|MFESTFLG_UUID) ){ + if( eflg & MFESTFLG_RAW ){ + blob_append(&filename, "manifest", -1); + zName = blob_str(&filename); + zip_add_folders(zName); + } + if( eflg & MFESTFLG_UUID ){ + sha1sum_blob(&mfile, &hash); + } + if( eflg & MFESTFLG_RAW ){ + sterilize_manifest(&mfile); + zip_add_file(zName, &mfile, 0); + } + } blob_reset(&mfile); - blob_append(&hash, "\n", 1); - blob_resize(&filename, nPrefix); - blob_append(&filename, "manifest.uuid", -1); - zName = blob_str(&filename); - zip_add_file(zName, &hash, 0); - blob_reset(&hash); + if( eflg & MFESTFLG_UUID ){ + blob_append(&hash, "\n", 1); + blob_resize(&filename, nPrefix); + blob_append(&filename, "manifest.uuid", -1); + zName = blob_str(&filename); + zip_add_folders(zName); + zip_add_file(zName, &hash, 0); + blob_reset(&hash); + } + if( eflg & MFESTFLG_TAGS ){ + Blob tagslist; + blob_zero(&tagslist); + get_checkin_taglist(rid, &tagslist); + blob_resize(&filename, nPrefix); + blob_append(&filename, "manifest.tags", -1); + zName = blob_str(&filename); + zip_add_folders(zName); + zip_add_file(zName, &tagslist, 0); + blob_reset(&tagslist); + } } manifest_file_rewind(pManifest); while( (pFile = manifest_file_next(pManifest,0))!=0 ){ int fid; if( pInclude!=0 && !glob_match(pInclude, pFile->zName) ) continue; ADDED test/set-manifest.test Index: test/set-manifest.test ================================================================== --- /dev/null +++ test/set-manifest.test @@ -0,0 +1,155 @@ +# +# Copyright (c) 2016 D. Richard Hipp +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Simplified BSD License (also +# known as the "2-Clause License" or "FreeBSD License".) +# +# This program is distributed in the hope that it will be useful, +# but without any warranty; without even the implied warranty of +# merchantability or fitness for a particular purpose. +# +# Author contact information: +# drh@hwaci.com +# http://www.hwaci.com/drh/ +# +############################################################################ +# +# Test manifest setting +# + +# We need SHA1 to effectively test the manifest files produced by +# fossil. It looks like the one from tcllib is exactly what we need. +# On ActiveTcl, add it with teacup. On other platforms, YMMV. +# teacup install sha1 +package require sha1 + +proc file_contains {fname match} { + set fp [open $fname r] + set contents [read $fp] + close $fp + set lines [split $contents "\n"] + foreach line $lines { + if {[regexp $match $line]} { + return 1 + } + } + return 0 +} + +# We need a respository, so let it have one. +test_setup + +#### Verify classic behavior of the manifest setting + +# Setting is off by default, and there are no extra files. +fossil settings manifest +test "set-manifest-1" {[regexp {^manifest *$} $RESULT]} +set filelist [glob -nocomplain manifest*] +test "set-manifest-1-n" {[llength $filelist] == 0} + +# Classic behavior: TRUE value creates manifest and manifest.uuid +set truths [list true on 1] +foreach v $truths { + fossil settings manifest $v + test "set-manifest-2-$v" {$RESULT eq ""} + fossil settings manifest + test "set-manifest-2-$v-a" {[regexp "^manifest\\s+\\(local\\)\\s+$v\\s*$" $RESULT]} + set filelist [glob manifest*] + test "set-manifest-2-$v-n" {[llength $filelist] == 2} + foreach f $filelist { + test "set-manifest-2-$v-f-$f" {[file isfile $f]} + } +} + +# ... and manifest.uuid is the checkout's hash +fossil info +regexp {(?m)^checkout:\s+([0-9a-f]{40})\s.*$} $RESULT ckoutline ckid +set uuid [string trim [read_file "manifest.uuid"]] +test "set-manifest-2-uuid" {$ckid eq $uuid} + +# ... which is also the SHA1 of the file "manifest" before it was +# sterilized by appending an extra line when writing the file. The +# extra text begins with # and is a full line, so we'll just strip +# it with a brute-force substitution. This probably has the right +# effect even if the checkin was PGP-signed, but we don't have that +# setting turned on for this manifest in any case. +regsub {(?m)^#.*\n} [read_file "manifest"] "" manifest +set muuid [::sha1::sha1 $manifest] +test "set-manifest-2-manifest" {$muuid eq $uuid} + + +# Classic behavior: FALSE value removes manifest and manifest.uuid +set falses [list false off 0] +foreach v $falses { + fossil settings manifest $v + test "set-manifest-3-$v" {$RESULT eq ""} + fossil settings manifest + test "set-manifest-3-$v-a" {[regexp "^manifest\\s+\\(local\\)\\s+$v\\s*$" $RESULT]} + set filelist [glob -nocomplain manifest*] + test "set-manifest-3-$v-n" {[llength $filelist] == 0} +} + + +# Classic behavior: unset removes manifest and manifest.uuid +fossil unset manifest +test "set-manifest-4" {$RESULT eq ""} +fossil settings manifest +test "set-manifest-4-a" {[regexp {^manifest *$} $RESULT]} +set filelist [glob -nocomplain manifest*] +test "set-manifest-4-n" {[llength $filelist] == 0} + + +##### Tags Manifest feature extends the manifest setting + +# Manifest Tags: use letters r, u, and t to select each of manifest, +# manifest.uuid, and manifest.tags files. +set truths [list r u t ru ut rt rut] +foreach v $truths { + fossil settings manifest $v + test "set-manifest-5-$v" {$RESULT eq ""} + fossil settings manifest + test "set-manifest-5-$v-a" {[regexp "^manifest\\s+\\(local\\)\\s+$v\\s*$" $RESULT]} + set filelist [glob manifest*] + test "set-manifest-5-$v-n" {[llength $filelist] == [string length $v]} + foreach f $filelist { + test "set-manifest-5-$v-f-$f" {[file isfile $f]} + } +} + +# Quick check for tags applied in trunk +test_file_contents "set-manifest-6" "manifest.tags" "branch trunk\ntag trunk\n" + + +##### Test manifest.tags file content updates after commits + +# Explicitly set manifest.tags mode +fossil set manifest t +test "set-manifest-7-1" {[file isfile manifest.tags]} + +# Add a tag and make sure it appears in manifest.tags +fossil tag add manifest-7-tag-1 tip +test "set-manifest-7-2" {[file_contains "manifest.tags" "^tag manifest-7-tag-1$"]} + +# Add a file and make sure tag has disappeared from manifest.tags +write_file file1 "file1 contents" +fossil add file1 +fossil commit -m "Added file1." +test "set-manifest-7-3" {![file_contains "manifest.tags" "^tag manifest-7-tag-1$"]} + +# Add new tag and check that it is in manifest.tags +fossil tag add manifest-7-tag-2 tip +test "set-manifest-7-4" {[file_contains "manifest.tags" "^tag manifest-7-tag-2$"]} + + +##### Tags manifest branch= updates + +# Add file, create new branch on commit and check that +# manifest.tags has been updated appropriately +write_file file3 "file3 contents" +fossil add file3 +fossil commit -m "Added file3." --branch manifest-8-branch +test "set-manifest-8" {[file_contains "manifest.tags" "^branch manifest-8-branch$"]} + + +test_cleanup