Index: src/add.c ================================================================== --- src/add.c +++ src/add.c @@ -78,15 +78,16 @@ */ static const struct { const char *fname; int flg; }aManifestflags[] = { - { "manifest", MFESTFLG_RAW }, - { "manifest.uuid", MFESTFLG_UUID }, - { "manifest.tags", MFESTFLG_TAGS } + { "manifest", MFESTFLG_RAW }, + { "manifest.uuid", MFESTFLG_UUID }, + { "manifest.tags", MFESTFLG_TAGS }, + { "manifest.symlinks", MFESTFLG_SYMLINKS } }; - static const char *azManifests[3]; + static const char *azManifests[4]; /* ** Names of repository files, if they exist in the checkout. */ static const char *azRepo[4] = { 0, 0, 0, 0 }; Index: src/checkin.c ================================================================== --- src/checkin.c +++ src/checkin.c @@ -1499,10 +1499,16 @@ Blob mcksum; /* Manifest checksum */ ManifestFile *pFile; /* File from the baseline */ int nFBcard = 0; /* Number of B-cards and F-cards */ int i; /* Loop counter */ const char *zColor; /* Modified value of p->zColor */ + + /* On Windows, get symlink permission status from the "manifest.symlinks" file + ** if it exists and if the "manifest" setting contains the "l" flag. */ +#ifdef _WIN32 + int manifestSymlinks = get_checkout_symlink_table(); +#endif assert( pBaseline==0 || pBaseline->zBaseline==0 ); assert( pBaseline==0 || zBaselineUuid!=0 ); blob_zero(pOut); if( vid ){ @@ -1553,16 +1559,22 @@ int cmp; blob_resize(&filename, nBasename); blob_append(&filename, zName, -1); -#if !defined(_WIN32) + /* Potentially update the permissions of files selected to be checked in. */ +#ifdef _WIN32 + /* For Windows, if the "manifest" setting contains the "l" flag and the + ** "manifest.symlinks" file exists, use its contents to determine which + ** files do and do not have the symlink permission. */ + if( isSelected && manifestSymlinks ){ + isLink = db_exists("SELECT 1 FROM symlink_perm WHERE filename=%Q", zName); + } +#else /* For unix, extract the "executable" and "symlink" permissions ** directly from the filesystem. On windows, permissions are - ** unchanged from the original. However, only do this if the file - ** itself is actually selected to be part of this check-in. - */ + ** unchanged from the original. */ if( isSelected ){ int mPerm; mPerm = file_wd_perm(blob_str(&filename)); isExe = ( mPerm==PERM_EXE ); @@ -2613,13 +2625,23 @@ get_checkin_taglist(nvid, &tagslist); blob_write_to_file(&tagslist, zManifestFile); blob_reset(&tagslist); free(zManifestFile); } + + if( outputManifest & MFESTFLG_SYMLINKS ){ + Blob symlinkslist; + zManifestFile = mprintf("%smanifest.symlinks", g.zLocalRoot); + blob_zero(&symlinkslist); + get_checkin_symlinklist(nvid, &symlinkslist); + blob_write_to_file(&symlinkslist, zManifestFile); + blob_reset(&symlinkslist); + free(zManifestFile); + } if( !g.markPrivate ){ autosync_loop(SYNC_PUSH|SYNC_PULL, db_get_int("autosync-tries", 1), 0); } if( count_nonbranch_children(vid)>1 ){ fossil_print("**** warning: a fork has occurred *****\n"); } } Index: src/checkout.c ================================================================== --- src/checkout.c +++ src/checkout.c @@ -93,12 +93,11 @@ db_reset(&s); } /* ** Set or clear the execute permission bit (as appropriate) for all -** files in the current check-out, and replace files that have -** symlink bit with actual symlinks. +** files in the current check-out. */ void checkout_set_all_exe(int vid){ Blob filename; int baseLen; Manifest *pManifest; @@ -128,17 +127,18 @@ /* ** 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". +** each character as a flag to enable writing "manifest", "manifest.uuid", +** "manifest.tags", or "manifest.symlinks". */ void manifest_to_disk(int vid){ char *zManFile; Blob manifest; Blob taglist; + Blob symlinklist; int flg; flg = db_get_manifest_setting(); if( flg & MFESTFLG_RAW ){ @@ -182,10 +182,24 @@ zManFile = mprintf("%smanifest.tags", g.zLocalRoot); file_delete(zManFile); free(zManFile); } } + if( flg & MFESTFLG_SYMLINKS ){ + blob_zero(&symlinklist); + zManFile = mprintf("%smanifest.symlinks", g.zLocalRoot); + get_checkin_symlinklist(vid, &symlinklist); + blob_write_to_file(&symlinklist, zManFile); + free(zManFile); + blob_reset(&symlinklist); + }else{ + if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest.symlinks'") ){ + zManFile = mprintf("%smanifest.symlinks", g.zLocalRoot); + file_delete(zManFile); + free(zManFile); + } + } } /* ** Find the branch name and all symbolic tags for a particular check-in ** identified by "rid". @@ -216,10 +230,86 @@ } db_reset(&stmt); db_finalize(&stmt); } +/* +** Store into "pOut" a sorted list of all symlinks in a checkin "rid". Each +** entry in the list is the full name of the symlink (not the name of its +** target) followed by a newline. +*/ +void get_checkin_symlinklist(int rid, Blob *pOut){ + Manifest *pManifest = manifest_get(rid, CFTYPE_MANIFEST, 0); + ManifestFile *pFile; + blob_reset(pOut); + manifest_file_rewind(pManifest); + while( (pFile = manifest_file_next(pManifest, 0)) ){ + if( pFile->zPerm && strstr(pFile->zPerm, "l") ){ + blob_appendf(pOut, "%s\n", pFile->zName); + } + } + manifest_destroy(pManifest); +} + +#ifdef _WIN32 +/* +** Create a temporary table called "symlink_perm" containing the names of all +** files considered to be symlinks. This function only exists in Windows +** because Unix symlink status comes directly from the filesystem. The return +** value is 1 if the table was created or 0 if symlink status is to be inherited +** from the baseline check-in manifest. The latter case occurs when the file +** does not exist or when the "manifest" setting does not contain the "l" flag. +*/ +int get_checkout_symlink_table(void){ + Blob content = BLOB_INITIALIZER; + char *zFile, *zLine, *zEnd; + + /* If the "manifest" setting lacks the "l" flag, do no further processing. + * Symlink status will be inherited from the previous check-in. */ + if( !(db_get_manifest_setting() & MFESTFLG_SYMLINKS) ){ + return 0; + } + + /* If the "manifest.symlinks" file does not exist, act as if the "manifest" + * setting didn't have "l". The file will be regenerated with the next commit + * or update, but for now, temporarily disable symlink status updating. */ + zFile = mprintf("%smanifest.symlinks", g.zLocalRoot); + if( file_wd_size(zFile)<0 ){ + return 0; + } + + /* Read "manifest.symlinks" into a blob to be analyzed. Simplify processing + * by forcing it to end with newline. (Blobs are always NUL-terminated.) */ + blob_read_from_file(&content, zFile); + blob_append(&content, "\n", 1); + zLine = blob_buffer(&content); + + /* Insert each non-empty line of "manifest.symlinks" into the "symlink_perm" + * temporary table. */ + db_begin_transaction(); + db_multi_exec("CREATE TEMP TABLE IF NOT EXISTS symlink_perm(" + "filename TEXT PRIMARY KEY %s)", filename_collation()); + while( *zLine ){ + /* Find end of line and replace with NUL. */ + for( zEnd = zLine; *zEnd!='\r' && *zEnd!='\n'; ++zEnd ); + *zEnd = 0; + + /* If not a blank line, insert filename into symlink table. */ + if( *zLine ){ + db_multi_exec("INSERT OR IGNORE INTO symlink_perm VALUES(%Q)", zLine); + } + + /* Find start of next line, or find terminating NUL at end of file. */ + for( zLine = zEnd+1; *zLine=='\r' || *zLine=='\n'; ++zLine ); + } + db_end_transaction(0); + blob_reset(&content); + + /* Let the caller know the "symlink_perm" table was created and is valid. */ + return 1; +} +#endif /* ** COMMAND: checkout* ** COMMAND: co* ** Index: src/db.c ================================================================== --- src/db.c +++ src/db.c @@ -2475,13 +2475,14 @@ 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 +#define MFESTFLG_RAW 0x01 +#define MFESTFLG_UUID 0x02 +#define MFESTFLG_TAGS 0x04 +#define MFESTFLG_SYMLINKS 0x08 #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 @@ -2498,13 +2499,14 @@ 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; + case 'r': flg |= MFESTFLG_RAW; break; + case 'u': flg |= MFESTFLG_UUID; break; + case 't': flg |= MFESTFLG_TAGS; break; + case 'l': flg |= MFESTFLG_SYMLINKS; break; } zVal++; } return flg; } @@ -2585,12 +2587,13 @@ ** Usage: %fossil open FILENAME ?VERSION? ?OPTIONS? ** ** Open a connection to the local repository in FILENAME. A checkout ** for the repository is created with its root at the working directory. ** If VERSION is specified then that version is checked out. Otherwise -** the latest version is checked out. No files other than "manifest" -** and "manifest.uuid" are modified if the --keep option is present. +** the latest version is checked out. If the --keep option is present, +** no files other than "manifest", "manifest.uuid", "manifest.tags", and +** "manifest.symlinks" are modified. ** ** Options: ** --empty Initialize checkout as being empty, but still connected ** with the local repository. If you commit this checkout, ** it will become a new "initial" commit in the repository. @@ -2982,13 +2985,13 @@ /* ** SETTING: manifest width=5 versionable ** If enabled, automatically create 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. +** Optionally use combinations of characters 'r' for "manifest", 'u' for +** "manifest.uuid", 't' for "manifest.tags", and 'l' for "manifest.symlinks". +** The SQLite and Fossil repositories both require manifests. */ /* ** SETTING: max-loadavg width=25 default=0.0 ** Some CPU-intensive web pages (ex: /zip, /tarball, /blame) ** are disallowed if the system load average goes above this Index: src/tar.c ================================================================== --- src/tar.c +++ src/tar.c @@ -514,10 +514,15 @@ if( (pInclude==0 || glob_match(pInclude, "manifest.tags")) && !glob_match(pExclude, "manifest.tags") && (flg & MFESTFLG_TAGS) ){ eflg |= MFESTFLG_TAGS; } + if( (pInclude==0 || glob_match(pInclude, "manifest.symlinks")) + && !glob_match(pExclude, "manifest.symlinks") + && (flg & MFESTFLG_SYMLINKS) ){ + eflg |= MFESTFLG_SYMLINKS; + } if( eflg & (MFESTFLG_RAW|MFESTFLG_UUID) ){ if( eflg & MFESTFLG_RAW ){ blob_append(&filename, "manifest", -1); zName = blob_str(&filename); @@ -542,10 +547,20 @@ blob_resize(&filename, nPrefix); blob_append(&filename, "manifest.tags", -1); zName = blob_str(&filename); tar_add_file(zName, &tagslist, 0, mTime); blob_reset(&tagslist); + } + if( eflg & MFESTFLG_SYMLINKS ){ + Blob symlinklist; + blob_zero(&symlinklist); + get_checkin_symlinklist(rid, &symlinklist); + blob_resize(&filename, nPrefix); + blob_append(&filename, "manifest.symlinks", -1); + zName = blob_str(&filename); + tar_add_file(zName, &symlinklist, 0, mTime); + blob_reset(&symlinklist); } } manifest_file_rewind(pManifest); while( (pFile = manifest_file_next(pManifest,0))!=0 ){ int fid; Index: src/vfile.c ================================================================== --- src/vfile.c +++ src/vfile.c @@ -173,10 +173,17 @@ void vfile_check_signature(int vid, unsigned int cksigFlags){ int nErr = 0; Stmt q; int useMtime = (cksigFlags & CKSIG_HASH)==0 && db_get_boolean("mtime-changes", 1); + + /* On Windows, get symlink permission status from the "manifest.symlinks" file + ** if it exists and if the "manifest" setting contains the "l" flag. */ +#ifdef _WIN32 + int manifestSymlinks = get_checkout_symlink_table(); + int nRoot = strlen(g.zLocalRoot); +#endif db_begin_transaction(); db_prepare(&q, "SELECT id, %Q || pathname," " vfile.mrid, deleted, chnged, uuid, size, mtime," " CASE WHEN isexe THEN %d WHEN islink THEN %d ELSE %d END" @@ -186,14 +193,12 @@ while( db_step(&q)==SQLITE_ROW ){ int id, rid, isDeleted; const char *zName; int chnged = 0; int oldChnged; -#ifndef _WIN32 int origPerm; int currentPerm; -#endif i64 oldMtime; i64 currentMtime; i64 origSize; i64 currentSize; @@ -204,12 +209,24 @@ oldChnged = chnged = db_column_int(&q, 4); oldMtime = db_column_int64(&q, 7); origSize = db_column_int64(&q, 6); currentSize = file_wd_size(zName); currentMtime = file_wd_mtime(0); -#ifndef _WIN32 origPerm = db_column_int(&q, 8); +#ifdef _WIN32 + /* For Windows, if the "manifest" setting contains the "l" flag and the + ** "manifest.symlinks" file exists, use its contents to determine which + ** files do and do not have the symlink permission. */ + if( !manifestSymlinks ){ + currentPerm = origPerm; + }else if( db_exists("SELECT 1 FROM symlink_perm " + "WHERE filename=%Q", zName+nRoot) ){ + currentPerm = PERM_LNK; + }else{ + currentPerm = 0; + } +#else currentPerm = file_wd_perm(zName); #endif if( chnged==0 && (isDeleted || rid==0) ){ /* "fossil rm" or "fossil add" always change the file */ chnged = 1; @@ -251,27 +268,27 @@ file_set_mtime(zName, desiredMtime); currentMtime = file_wd_mtime(zName); } } } -#ifndef _WIN32 if( origPerm!=PERM_LNK && currentPerm==PERM_LNK ){ /* Changing to a symlink takes priority over all other change types. */ chnged = 7; }else if( chnged==0 || chnged==6 || chnged==7 || chnged==8 || chnged==9 ){ /* Confirm metadata change types. */ if( origPerm==currentPerm ){ chnged = 0; +#ifndef _WIN32 }else if( currentPerm==PERM_EXE ){ chnged = 6; }else if( origPerm==PERM_EXE ){ chnged = 8; +#endif }else if( origPerm==PERM_LNK ){ chnged = 9; } } -#endif if( currentMtime!=oldMtime || chnged!=oldChnged ){ db_multi_exec("UPDATE vfile SET mtime=%lld, chnged=%d WHERE id=%d", currentMtime, chnged, id); } } Index: src/zip.c ================================================================== --- src/zip.c +++ src/zip.c @@ -371,10 +371,15 @@ if( (pInclude==0 || glob_match(pInclude, "manifest.tags")) && !glob_match(pExclude, "manifest.tags") && (flg & MFESTFLG_TAGS) ){ eflg |= MFESTFLG_TAGS; } + if( (pInclude==0 || glob_match(pInclude, "manifest.symlinks")) + && !glob_match(pExclude, "manifest.symlinks") + && (flg & MFESTFLG_SYMLINKS) ){ + eflg |= MFESTFLG_SYMLINKS; + } if( eflg & MFESTFLG_RAW ){ blob_append(&filename, "manifest", -1); zName = blob_str(&filename); zip_add_folders(zName); @@ -397,10 +402,21 @@ blob_append(&filename, "manifest.tags", -1); zName = blob_str(&filename); zip_add_folders(zName); zip_add_file(zName, &tagslist, 0); blob_reset(&tagslist); + } + if( eflg & MFESTFLG_SYMLINKS ){ + Blob symlinklist; + blob_zero(&symlinklist); + get_checkin_symlinklist(rid, &symlinklist); + blob_resize(&filename, nPrefix); + blob_append(&filename, "manifest.symlinks", -1); + zName = blob_str(&filename); + zip_add_folders(zName); + zip_add_file(zName, &symlinklist, 0); + blob_reset(&symlinklist); } } manifest_file_rewind(pManifest); while( (pFile = manifest_file_next(pManifest,0))!=0 ){ int fid; Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -30,10 +30,14 @@ [/help?cmd=zip|zip], and [/help?cmd=tarball|tarball] pages and commands to honor the versioned manifest setting when outside of an open checkout directory. * The admin-log and access-log settings are now on by default for new repositories. + * Add support for the "l" flag to the "manifest" setting to enable creation + of a file "manifest.symlinks" which lists the names of all symlinks. On + Windows, this new file can be edited to change which files are and are not + considered to be symlinks. * Update the built-in SQLite to version 3.20.1.

Changes for Version 2.3 (2017-07-21)