/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ #if !defined(ORG_FOSSIL_SCM_FSL_CHECKOUT_H_INCLUDED) #define ORG_FOSSIL_SCM_FSL_CHECKOUT_H_INCLUDED /* Copyright 2013-2021 The Libfossil Authors, see LICENSES/BSD-2-Clause.txt SPDX-License-Identifier: BSD-2-Clause-FreeBSD SPDX-FileCopyrightText: 2021 The Libfossil Authors SPDX-ArtifactOfProjectName: Libfossil SPDX-FileType: Code Heavily indebted to the Fossil SCM project (https://fossil-scm.org). */ /** @file fossil-checkout.h fossil-checkout.h declares APIs specifically dealing with checkout-side state, as opposed to purely repository-db-side state or non-content-related APIs. */ #include "fossil-db.h" /* MUST come first b/c of config macros */ #include "fossil-repo.h" #if defined(__cplusplus) extern "C" { #endif /** Returns version information for the current checkout. If f has an opened checkout then... If uuid is not NULL then *uuid is set to the UUID of the opened checkout, or NULL if there is no checkout. If rid is not NULL, *rid is set to the record ID of that checkout, or 0 if there is no checkout (or the current checkout is from an empty repository). The returned uuid bytes and rid are owned by f and valid until the library updates its checkout state to a newer checkout version (essentially unpredictably). When in doubt about lifetime issues, copy the UUID immediately after calling this if they will be needed later. Corner case: a new repo with no checkins has an RID of 0 and a UUID of NULL. That does not happen with fossil-generated repositories, as those always "seed" the database with an initial commit artifact containing no files. */ FSL_EXPORT void fsl_ckout_version_info(fsl_cx * const f, fsl_id_t * const rid, fsl_uuid_cstr * const uuid ); /** Given a fsl_cx with an opened checkout, and a filename, this function canonicalizes zOrigName to a form suitable for use as an in-repo filename, _appending_ the results to pOut. If pOut is NULL, it performs its normal checking but does not write a result, other than to return 0 for success. As a special case, if zOrigName refers to the top-level checkout directory, it resolves to either "." or "./", depending on whether zOrigName contains a trailing slash. If relativeToCwd is true then the filename is canonicalized based on the current working directory (see fsl_getcwd()), otherwise f's current checkout directory is used as the virtual root. If the input name contains a trailing slash, it is retained in the output sent to pOut except in the top-dir case mentioned above. Returns 0 on success, meaning that the value appended to pOut (if not NULL) is a syntactically valid checkout-relative path. Returns FSL_RC_RANGE if zOrigName points to a path outside of f's current checkout root. Returns FSL_RC_NOT_A_CKOUT if f has no checkout opened. Returns FSL_RC_MISUSE if !zOrigName, FSL_RC_OOM on an allocation error. This function does not validate whether or not the file actually exists, only that its name is potentially valid as a filename for use in a checkout (though other, downstream rules might prohibit that, e.g. the filename "..../...." is not valid but is not seen as invalid by this function). (Reminder to self: we could run the end result through fsl_is_simple_pathname() to catch that?) */ FSL_EXPORT int fsl_ckout_filename_check( fsl_cx * f, bool relativeToCwd, char const * zOrigName, fsl_buffer * pOut ); /** Callback type for use with fsl_ckout_manage_opt(). It should inspect the given filename using whatever criteria it likes, set *include to true or false to indicate whether the filename is okay to include the current add-file-to-repo operation, and return 0. If it returns non-0 the add-file-to-repo process will end and that error code will be reported to its caller. Such result codes must come from the FSL_RC_xxx family. It will be passed a name which is relative to the top-most checkout directory. The final argument is not used by the library, but is passed on as-is from the fsl_ckout_manage_opt::callbackState pointer which is passed to fsl_ckout_manage(). */ typedef int (*fsl_ckout_manage_f)(const char *zFilename, bool *include, void *state); /** Options for use with fsl_ckout_manage(). */ struct fsl_ckout_manage_opt { /** The file or directory name to add. If it is a directory, the add process will recurse into it. */ char const * filename; /** Whether to evaluate the given name as relative to the current working directory or to the current checkout root. This makes a subtle yet important difference in how the name is resolved. CLI apps which take file names from the user from within a checkout directory will generally want to set relativeToCwd to true. GUI apps, OTOH, will possibly need it to be false, depending on how they resolve and pass on the filenames. */ bool relativeToCwd; /** Whether or not to check the name(s) against the 'ignore-globs' config setting (if set). */ bool checkIgnoreGlobs; /** Optional predicate function which may be called for each to-be-added filename. It is only called if: - It is not NULL (obviously) and... - The file is not already in the checkout database and... - The is-internal-name check passes (see fsl_reserved_fn_check()) and... - If checkIgnoreGlobs is false or the name does not match one of the ignore-globs values. The name it is passed is relative to the checkout root. Because the callback is called only if other options have not already excluded the file, the client may use the callback to report to the user (or otherwise record) exactly which files get added. */ fsl_ckout_manage_f callback; /** State to be passed to this->callback. */ void * callbackState; /** These counts are updated by fsl_ckout_manage() to report what it did. */ struct { /** Number of files actually added by fsl_ckout_manage(). */ uint32_t added; /** Number of files which were requested to be added but were only updated because they had previously been added. Updates set the vfile table entry's current mtime, executable-bit state, and is-it-a-symlink state. (That said, this code currently ignores symlinks altogether!) */ uint32_t updated; /** The number of files skipped over for addition. This includes files which meet any of these criteria: - fsl_reserved_fn_check() fails. - If the checkIgnoreGlobs option is true and a filename matches any of those globs. - The client-provided callback says not to include the file. */ uint32_t skipped; } counts; }; typedef struct fsl_ckout_manage_opt fsl_ckout_manage_opt; /** Initialized-with-defaults fsl_ckout_manage_opt instance, intended for use in const-copy initialization. */ #define fsl_ckout_manage_opt_empty_m {\ NULL/*filename*/, true/*relativeToCwd*/, true/*checkIgnoreGlobs*/, \ NULL/*callback*/, NULL/*callbackState*/, \ {/*counts*/ 0/*added*/, 0/*updated*/, 0/*skipped*/} \ } /** Initialized-with-defaults fsl_ckout_manage_opt instance, intended for use in non-const copy initialization. */ FSL_EXPORT const fsl_ckout_manage_opt fsl_ckout_manage_opt_empty; /** Adds the given filename or directory (recursively) to the current checkout vfile list of files as a to-be-added file, or updates an existing record if one exists. This function ensures that opt->filename gets canonicalized and can be found under the checkout directory, and fails if no such file exists (checking against the canonicalized name). Filenames are all filtered through fsl_reserved_fn_check() and may have other filters applied to them, as determined by the options object. Each filename which passes through the filters is passed to the opt->callback (if not NULL), which may perform a final filtering check and/or alert the client about the file being queued. The options object is non-const because this routine updates opt->counts when it adds, updates, or skips a file. On each call, it updated opt->counts without resetting it (as this function is typically called in a loop). This function does not modify any other entries of that object and it requires that the object not be modified (e.g. via opt->callback()) while it is recursively processing. To reset the counts between calls, if needed: ``` opt->counts = fsl_ckout_manage_opt_empty.counts; ``` Returns 0 on success, non-0 on error. Files queued for addition this way can be unqueued before they are committed using fsl_ckout_unmanage(). @see fsl_ckout_unmanage() @see fsl_reserved_fn_check() */ FSL_EXPORT int fsl_ckout_manage( fsl_cx * const f, fsl_ckout_manage_opt * const opt ); /** Callback type for use with fsl_ckout_unmanage(). It is called by the removal process, immediately after a file is "removed" from SCM management (a.k.a. when the file becomes "unmanaged"). If it returns non-0 the unmanage process will end and that error code will be reported to its caller. Such result codes must come from the FSL_RC_xxx family. It will be passed a name which is relative to the top-most checkout directory. The client is free to unlink the file from the filesystem if they like - the library does not do so automatically The final argument is not used by the library, but is passed on as-is from the callbackState pointer which is passed to fsl_ckout_unmanage(). */ typedef int (*fsl_ckout_unmanage_f)(const char *zFilename, void *state); /** Options for use with fsl_ckout_unmanage(). */ struct fsl_ckout_unmanage_opt { /** The file or directory name to add. If it is a directory, the add process will recurse into it. See also this->vfileIds. */ char const * filename; /** An alternative to assigning this->filename is to point this->vfileIds to a bag of vfile.id values. If this member is not NULL, fsl_ckout_unmanage() will ignore this->filename. @see fsl_filename_to_vfile_ids() */ fsl_id_bag const * vfileIds; /** Whether to evaluate this->filename as relative to the current working directory (true) or to the current checkout root (false). This is ignored when this->vfileIds is not NULL. This makes a subtle yet important difference in how the name is resolved. CLI apps which take file names from the user from within a checkout directory will generally want to set relativeToCwd to true. GUI apps, OTOH, will possibly need it to be false, depending on how they resolve and pass on the filenames. */ bool relativeToCwd; /** If true, fsl_vfile_changes_scan() is called to ensure that the filesystem and vfile tables agree. If the client code has called that function, or its equivalent, since any changes were made to the checkout then this may be set to false to speed up the rm process. */ bool scanForChanges; /** Optional predicate function which will be called after each file is made unmanaged. The name it is passed is relative to the checkout root. */ fsl_ckout_unmanage_f callback; /** State to be passed to this->callback. */ void * callbackState; }; typedef struct fsl_ckout_unmanage_opt fsl_ckout_unmanage_opt; /** Initialized-with-defaults fsl_ckout_unmanage_opt instance, intended for use in const-copy initialization. */ #define fsl_ckout_unmanage_opt_empty_m {\ NULL/*filename*/, NULL/*vfileIds*/,\ true/*relativeToCwd*/,true/*scanForChanges*/, \ NULL/*callback*/, NULL/*callbackState*/ \ } /** Initialized-with-defaults fsl_ckout_unmanage_opt instance, intended for use in non-const copy initialization. */ FSL_EXPORT const fsl_ckout_unmanage_opt fsl_ckout_unmanage_opt_empty; /** The converse of fsl_ckout_manage(), this queues a file for removal from the current checkout. Unlike fsl_ckout_manage(), this routine does not ensure that opt->filename actually exists - it only normalizes zFilename into its repository-friendly form and passes it through the vfile table. If opt->filename refers to a directory then this operation queues all files under that directory (recursively) for removal. In this case, it is irrelevant whether or not opt->filename ends in a trailing slash. Returns 0 on success, any of a number of non-0 codes on error. Returns FSL_RC_MISUSE if !opt->filename or !*opt->filename. Returns FSL_RC_NOT_A_CKOUT if f has no opened checkout. If opt->callback is not NULL, it is called for each newly-unamanaged entry. The intention is to provide it the opportunity to notify the user, record the filename for later use, remove the file from the filesystem, etc. If it returns non-0, the unmanaging process will fail with that code and any pending transaction will be placed into a rollback state. This routine does not actually remove any files from the filesystem, it only modifies the vfile table entry so that the file(s) will be removed from the SCM by the commit process. If opt->filename is an entry which was previously fsl_ckout_manage()'d, but not yet committed, or any such entries are found under directory opt->filename, they are removed from the vfile table. i.e. this effective undoes the add operation. @see fsl_ckout_manage() */ FSL_EXPORT int fsl_ckout_unmanage( fsl_cx * f, fsl_ckout_unmanage_opt const * opt ); /** Hard-coded range of values of the vfile.chnged db field. These values are part of the fossil schema and must not be modified. */ enum fsl_vfile_change_e { /** File is unchanged. */ FSL_VFILE_CHANGE_NONE = 0, /** File edit. */ FSL_VFILE_CHANGE_MOD = 1, /** File changed due to a merge. */ FSL_VFILE_CHANGE_MERGE_MOD = 2, /** File added by a merge. */ FSL_VFILE_CHANGE_MERGE_ADD = 3, /** File changed due to an integrate merge. */ FSL_VFILE_CHANGE_INTEGRATE_MOD = 4, /** File added by an integrate merge. */ FSL_VFILE_CHANGE_INTEGRATE_ADD = 5, /** File became executable but has unmodified contents. */ FSL_VFILE_CHANGE_IS_EXEC = 6, /** File became a symlink whose target equals its old contents. */ FSL_VFILE_CHANGE_BECAME_SYMLINK = 7, /** File lost executable status but has unmodified contents. */ FSL_VFILE_CHANGE_NOT_EXEC = 8, /** File lost symlink status and has contents equal to its old target. */ FSL_VFILE_CHANGE_NOT_SYMLINK = 9 }; typedef enum fsl_vfile_change_e fsl_vfile_change_e; /** Change-type flags for use with fsl_ckout_changes_visit() and friends. TODO: consolidate this with fsl_vfile_change_e insofar as possible. There are a few checkout change statuses not reflected in fsl_vfile_change_e. */ enum fsl_ckout_change_e { /** Sentinel placeholder value. */ FSL_CKOUT_CHANGE_NONE = 0, /** Indicates that a file was modified in some unspecified way. */ FSL_CKOUT_CHANGE_MOD = FSL_VFILE_CHANGE_MOD, /** Indicates that a file was modified as the result of a merge. */ FSL_CKOUT_CHANGE_MERGE_MOD = FSL_VFILE_CHANGE_MERGE_MOD, /** Indicates that a file was added as the result of a merge. */ FSL_CKOUT_CHANGE_MERGE_ADD = FSL_VFILE_CHANGE_MERGE_ADD, /** Indicates that a file was modified as the result of an integrate-merge. */ FSL_CKOUT_CHANGE_INTEGRATE_MOD = FSL_VFILE_CHANGE_INTEGRATE_MOD, /** Indicates that a file was added as the result of an integrate-merge. */ FSL_CKOUT_CHANGE_INTEGRATE_ADD = FSL_VFILE_CHANGE_INTEGRATE_ADD, /** Indicates that the file gained the is-executable trait but is otherwise unmodified. */ FSL_CKOUT_CHANGE_IS_EXEC = FSL_VFILE_CHANGE_IS_EXEC, /** Indicates that the file has changed to a symlink. */ FSL_CKOUT_CHANGE_BECAME_SYMLINK = FSL_VFILE_CHANGE_BECAME_SYMLINK, /** Indicates that the file lost the is-executable trait but is otherwise unmodified. */ FSL_CKOUT_CHANGE_NOT_EXEC = FSL_VFILE_CHANGE_NOT_EXEC, /** Indicates that the file was previously a symlink but is now a plain file. */ FSL_CKOUT_CHANGE_NOT_SYMLINK = FSL_VFILE_CHANGE_NOT_SYMLINK, /** Indicates that a file was added. */ FSL_CKOUT_CHANGE_ADDED = FSL_CKOUT_CHANGE_NOT_SYMLINK + 1000, /** Indicates that a file was removed from SCM management. */ FSL_CKOUT_CHANGE_REMOVED, /** Indicates that a file is missing from the local checkout. */ FSL_CKOUT_CHANGE_MISSING, /** Indicates that a file was renamed. */ FSL_CKOUT_CHANGE_RENAMED }; typedef enum fsl_ckout_change_e fsl_ckout_change_e; /** This is equivalent to calling fsl_vfile_changes_scan() with the arguments (f, -1, 0). @see fsl_ckout_changes_visit() @see fsl_vfile_changes_scan() */ FSL_EXPORT int fsl_ckout_changes_scan(fsl_cx * f); /** A typedef for visitors of checkout status information via fsl_ckout_changes_visit(). Implementions will receive the last argument passed to fsl_ckout_changes_visit() as their first argument. The second argument indicates the type of change and the third holds the repository-relative name of the file. If changes is FSL_CKOUT_CHANGE_RENAMED then origName will hold the original name, else it will be NULL. Implementations must return 0 on success, non-zero on error. On error any looping performed by fsl_ckout_changes_visit() will stop and this function's result code will be returned. @see fsl_ckout_changes_visit() */ typedef int (*fsl_ckout_changes_f)(void * state, fsl_ckout_change_e change, char const * filename, char const * origName); /** Compares the changes of f's local checkout against repository version vid (checkout version if vid is negative). For each change detected it calls visitor(state,...) to report the change. If visitor() returns non-0, that code is returned from this function. If doChangeScan is true then fsl_ckout_changes_scan() is called by this function before iterating, otherwise it is assumed that the caller has called that or has otherwise ensured that the checkout db's vfile table has been populated. If the callback returns FSL_RC_BREAK, this function stops iteration and returns 0. Returns 0 on success. @see fsl_ckout_changes_scan() */ FSL_EXPORT int fsl_ckout_changes_visit( fsl_cx * f, fsl_id_t vid, bool doChangeScan, fsl_ckout_changes_f visitor, void * state ); /** A bitmask of flags for fsl_vfile_changes_scan(). */ enum fsl_ckout_sig_e { /** The empty flags set. */ FSL_VFILE_CKSIG_NONE = 0, /** Non-file/non-link FS objects trigger an error. */ FSL_VFILE_CKSIG_ENOTFILE = 0x001, /** Verify file content using hashing, regardless of whether or not file timestamps differ. */ FSL_VFILE_CKSIG_HASH = 0x002, /** For unchanged or changed-by-merge files, set the mtime to last check-out time, as determined by fsl_mtime_of_manifest_file(). */ FSL_VFILE_CKSIG_SETMTIME = 0x004, /** Indicates that when populating the vfile table, it should be not be cleared of entries for other checkins. Normally we want to clear all versions except for the one we're working with, but at least a couple of use cases call for having multiple versions in vfile at once. Many algorithms generally assume only a single checkin's worth of state is in vfile and can get confused if that is not the case. */ FSL_VFILE_CKSIG_KEEP_OTHERS = 0x008, /** If set and fsl_vfile_changes_scan() is passed a version other than the pre-call checkout version, it will, when finished, write the given version in the "checkout" setting of the ckout.vvar table, effectively switching the checkout to that version. It does not do this by default because it is sometimes necessary to have two versions in the vfile table at once and the operation doing so needs to control which version number is the current checkout. */ FSL_VFILE_CKSIG_WRITE_CKOUT_VERSION = 0x010 }; /** This function populates (if needed) the vfile table of f's checkout db for the given checkin version ID then compares files listed in it against files in the checkout directory, updating vfile's status for the current checkout version id as its goes. If vid is<=0 then the current checkout's RID is used in its place (note that 0 is the RID of an initial empty repository!). cksigFlags must be 0 or a bitmask of fsl_ckout_sig_e values. This is a relatively memory- and filesystem-intensive operation, and should not be performed more often than necessary. Many SCM algorithms rely on its state being correct, however, so it's generally better to err on the side of running it once too often rather than once too few times. Returns 0 on success, non-0 on error. BUG: this does not properly catch one particular corner-case change, where a file has been replaced by a same-named non-file (symlink or directory). */ FSL_EXPORT int fsl_vfile_changes_scan(fsl_cx * const f, fsl_id_t vid, unsigned cksigFlags); /** If f has an opened checkout which has local changes noted in its checkout db state (the vfile table), returns true, else returns false. Note that this function does not do the filesystem scan to check for changes, but checks only the db state. Use fsl_vfile_changes_scan() to perform the actual scan (noting that library-side APIs which update that state may also record individual changes or automatically run a scan). */ FSL_EXPORT bool fsl_ckout_has_changes(fsl_cx * const f); /** Callback type for use with fsl_checkin_queue_opt for alerting a client about exactly which files get enqueued/dequeued via fsl_checkin_enqueue() and fsl_checkin_dequeue(). This function gets passed the checkout-relative name of the file being enqueued/dequeued and the client-provided state pointer which was passed to the relevant API. It must return 0 on success. If it returns non-0, the API on whose behalf this callback is invoked will propagate that error code back to the caller. The intent of this callback is simply to report changes to the client, not to perform validation. Thus such callbacks "really should not fail" unless, e.g., they encounter an OOM condition or some such. Any validation required by the client should be performed before calling fsl_checkin_enqueue() resp. fsl_checkin_dequeue(). */ typedef int (*fsl_checkin_queue_f)(const char * filename, void * state); /** Options object type used by fsl_checkin_enqueue() and fsl_checkin_dequeue(). */ struct fsl_checkin_queue_opt { /** File or directory name to enqueue/dequeue to/from a pending checkin. */ char const * filename; /** If true, filename (if not absolute) is interpreted as relative to the current working directory, else it is assumed to be relative to the top of the current checkout directory. */ bool relativeToCwd; /** If not NULL then this->filename and this->relativeToCwd are IGNORED and any to-queue filename(s) is/are added from this container. It is an error (FSL_RC_MISUSE) to pass an empty bag. (Should that be FSL_RC_RANGE instead?) The bag is assumed to contain values from the vfile.id checkout db field, refering to one or more files which should be queued for the pending checkin. It is okay to pass IDs for unmodified files or to queue the same files multiple times. Unmodified files may be enqueued but will be ignored by the checkin process if, at the time the checkin is processed, they are still unmodified. Duplicated entries are simply ignored for the 2nd and subsequent inclusion. @see fsl_ckout_vfile_ids() */ fsl_id_bag const * vfileIds; /** If true, fsl_vfile_changes_scan() is called to ensure that the filesystem and vfile tables agree. If the client code has called that function, or its equivalent, since any changes were made to the checkout then this may be set to false to speed up the enqueue process. This is only used by fsl_checkin_enqueue(), not fsl_checkin_dequeue(). */ bool scanForChanges; /** If true, only flagged-as-modified files will be enqueued by fsl_checkin_enqueue(). By and large, this should be set to true. Setting this to false is generally only intended/useful for testing. */ bool onlyModifiedFiles; /** It not NULL, is pass passed the checkout-relative filename of each enqueued/dequeued file and this->callbackState. See the callback type's docs for more details. */ fsl_checkin_queue_f callback; /** Opaque client-side state for use as the 2nd argument to this->callback. */ void * callbackState; }; /** Convenience typedef. */ typedef struct fsl_checkin_queue_opt fsl_checkin_queue_opt; /** Initialized-with-defaults fsl_checkin_queue_opt structure, intended for const-copy initialization. */ #define fsl_checkin_queue_opt_empty_m { \ NULL/*filename*/,true/*relativeToCwd*/, \ NULL/*vfileIds*/, \ true/*scanForChanges*/,true/*onlyModifiedFiles*/, \ NULL/*callback*/,NULL/*callbackState*/ \ } /** Initialized-with-defaults fsl_checkin_queue_opt structure, intended for non-const copy initialization. */ extern const fsl_checkin_queue_opt fsl_checkin_queue_opt_empty; /** Adds one or more files to f's list of "selected" files - those which should be included in the next commit (see fsl_checkin_commit()). Warning: if this function is not called before fsl_checkin_commit(), then fsl_checkin_commit() will select all modified, fsl_ckout_manage()'d, fsl_ckout_unmanage()'d, or renamed files by default. opt->filename must be a non-empty NUL-terminated string. The filename is canonicalized via fsl_ckout_filename_check() - see that function for the meaning of the opt->relativeToCwd parameter. To queue all modified files in a checkout, set opt->filename to ".", opt->relativeToCwd to false, and opt->onlyModifiedFiles to true. "Modified" includes any which are pending deletion, are newly-added, or for which a rename is pending. The resolved name must refer to either a single vfile.pathname value in the current vfile table or to a checkout-root-relative directory. All matching filenames which refer to modified files (as recorded in the vfile table) are queued up for the next commit. If opt->filename is NULL, empty, or ("." and opt->relativeToCwd is false) then all files in the vfile table are checked for changes. If opt->scanForChanges is true then fsl_vfile_changes_scan() is called before starting to ensure that the vfile entries are up to date. If the client app has "recently" run that (or its equivalent), that (slow) step can be skipped by setting opt->scanForChanges to false before calling this Note that after this returns, any given file may still be modified by the client before the commit takes place, and the changes on disk at the point of the fsl_checkin_commit() are the ones which get saved (or not). For each resolved entry which actually gets enqueued (i.e. was not already enqueued and which is marked as modified), opt->callback (if it is not NULL) is passed the checkout-relative file name and the opt->callbackState pointer. Returns 0 on success, FSL_RC_MISUSE if either pointer is NULL, or *zName is NUL. Returns FSL_RC_OOM on allocation error. It is not inherently an error for opt->filename to resolve to no queue-able entries. A client can check for that case, if needed, by assigning opt->callback and incrementing a counter in that callback. If the callback is never called, no queue-able entries were found. On error f's error state might (depending on the nature of the problem) contain more details. @see fsl_checkin_is_enqueued() @see fsl_checkin_dequeue() @see fsl_checkin_discard() @see fsl_checkin_commit() */ FSL_EXPORT int fsl_checkin_enqueue(fsl_cx * f, fsl_checkin_queue_opt const * opt); /** The opposite of fsl_checkin_enqueue(), this opt->filename, which may resolve to a single name or a directory, from the checkin queue. Returns 0 on succes. This function does no validation on whether a given file(s) actually exist(s), it simply asks the internals to clean up matching strings from the checkout's vfile table. Specifically, it does not return an error if this operation finds no entries to dequeue. If opt->filename is empty or NULL then ALL files are unqueued from the pending checkin. If opt->relativeToCwd is true (non-0) then opt->filename is resolved based on the current directory, otherwise it is resolved based on the checkout's root directory. If opt->filename is not NULL or empty, this functions runs the given path through fsl_ckout_filename_check() and will fail if that function fails, propagating any error from that function. Ergo, opt->filename must refer to a path within the current checkout. @see fsl_checkin_enqueue() @see fsl_checkin_is_enqueued() @see fsl_checkin_discard() @see fsl_checkin_commit() */ FSL_EXPORT int fsl_checkin_dequeue(fsl_cx * f, fsl_checkin_queue_opt const * opt); /** Returns true if the file named by zName is in f's current file checkin queue. If NO files are in the current selection queue then this routine assumes that ALL files are implicitely selected. As long as at least one file is enqueued (via fsl_checkin_enqueue()) then this function only returns true for files which have been explicitly enqueued. If relativeToCwd then zName is resolved based on the current directory, otherwise it assumed to be related to the checkout's root directory. This function returning true does not necessarily indicate that the file _will_ be checked in at the next commit. If the file has not been modified at commit-time then it will not be part of the commit. This function honors the fsl_cx_is_case_sensitive() setting when comparing names. Achtung: this does not resolve directory names like fsl_checkin_enqueue() and fsl_checkin_dequeue() do. It only works with file names. Results are undefined if f is NULL. @see fsl_checkin_enqueue() @see fsl_checkin_dequeue() @see fsl_checkin_discard() @see fsl_checkin_commit() */ FSL_EXPORT bool fsl_checkin_is_enqueued(fsl_cx * const f, char const * zName, bool relativeToCwd); /** Discards any state accumulated for a pending checking, including any files queued via fsl_checkin_enqueue() and tags added via fsl_checkin_T_add(). @see fsl_checkin_enqueue() @see fsl_checkin_dequeue() @see fsl_checkin_is_enqueued() @see fsl_checkin_commit() @see fsl_checkin_T_add() */ FSL_EXPORT void fsl_checkin_discard(fsl_cx * f); /** Parameters for fsl_checkin_commit(). Checkins are created in a multi-step process: - fsl_checkin_enqueue() queues up a file or directory for commit at the next commit. - fsl_checkin_dequeue() removes an entry, allowing UIs to toggle files in and out of a checkin before committing it. - fsl_checkin_is_enqueued() can be used to determine whether a given name is already enqueued or not. - fsl_checkin_T_add() can be used to T-cards (tags) to a deck. Branch tags are intended to be applied via the fsl_checkin_opt::branch member. - fsl_checkin_discard() can be used to cancel any pending file enqueuings, effectively cancelling a commit (which can be re-started by enqueuing another file). - fsl_checkin_commit() creates a checkin for the list of enqueued files (defaulting to all modified files in the checkout!). It takes an object of this type to specify a variety of parameters for the check. Note that this API uses the terms "enqueue" and "unqueue" rather than "add" and "remove" because those both have very specific (and much different) meanings in the overall SCM scheme. */ struct fsl_checkin_opt { /** The commit message. May not be empty - the library forbids empty checkin messages. */ char const * message; /** The optional mime type for the message. Only set this if you know what you're doing. */ char const * messageMimeType; /** The user name for the checkin. If NULL or empty, it defaults to fsl_cx_user_get(). If that is NULL, a FSL_RC_RANGE error is triggered. */ char const * user; /** If not NULL, makes the checkin the start of a new branch with this name. */ char const * branch; /** If this->branch is not NULL, this is applied as its "bgcolor" propagating property. If this->branch is NULL then this is applied as a one-time color tag to the checkin. It must be NULL, empty, or in a form usable by HTML/CSS, preferably \#RRGGBB form. Length-0 values are ignored (as if they were NULL). */ char const * bgColor; /** If true, the checkin will be marked as private, otherwise it will be marked as private or public, depending on whether or not it inherits private content. */ bool isPrivate; /** Whether or not to calculate an R-card. Doing so is very expensive (memory and I/O) but it adds another layer of consistency checking to manifest files. In practice, the R-card is somewhat superfluous and the cost of calculating it has proven painful on very large repositories. fossil(1) creates an R-card for all checkins but does not require that one be set when it reads a manifest. */ bool calcRCard; /** Tells the checkin to close merged-in branches (merge type of 0). INTEGRATE merges (type=-4) are always closed by a checkin. This does not apply to CHERRYPICK (type=-1) and BACKOUT (type=-2) merges. */ bool integrate; /** If true, allow a file to be checked in if it contains fossil-style merge conflict markers, else fail if an attempt is made to commit any files with such markers. */ bool allowMergeConflict; /** A hint to fsl_checkin_commit() about whether it needs to scan the checkout for changes. Set this to false ONLY if the calling code calls fsl_ckout_changes_scan() (or equivalent, e.g. fsl_vfile_changes_scan()) immediately before calling fsl_checkin_commit(). fsl_checkin_commit() requires a non-stale changes scan in order to function properly, but it's a computationally slow operation so the checkin process does not want to duplicate it if the application has recently done so. */ bool scanForChanges; /** NOT YET IMPLEMENTED! TODO! If true, files which are removed from the SCM by this checkin should be removed from the filesystem. Reminder to self: when we do this, incorporate fsl_rm_empty_dirs(). */ bool rmRemovedFiles; /** Whether to allow (or try to force) a delta manifest or not. 0 means no deltas allowed - it will generate a baseline manifest. Greater than 0 forces generation of a delta if possible (if one can be readily found) even if doing so would not save a notable amount of space. Less than 0 means to decide via some heuristics. A "readily available" baseline means either the current checkout is a baseline or has a baseline. In either case, we can use that as a baseline for a delta. i.e. a baseline "should" generally be available except on the initial checkin, which has neither a parent checkin nor a baseline. The current behaviour for "auto-detect" mode is: it will generate a delta if a baseline is "readily available" _and_ the repository has at least one delta already. Once it calculates a delta form, it calculates whether that form saves any appreciable space/overhead compared to whether a baseline manifest was generated. If so, it discards the delta and re-generates the manifest as a baseline. The "force" behaviour (deltaPolicy>0) bypasses the "is it too big?" test, and is only intended for testing, not real-life use. Caveat: if the repository has the "forbid-delta-manifests" set to a true value, this option is ignored: that setting takes priority. Similarly, it will not create a delta in a repository unless a delta has been "seen" in that repository before or this policy is set to >0. When a checkin is created with a delta manifest, that fact gets recorded in the repository's config table. Note that delta manifests have some advantages and may not actually save much (if any) repository space because the lower-level delta framework already compresses parent versions of artifacts tightly. For more information see: https://fossil-scm.org/home/doc/tip/www/delta-manifests.md */ int deltaPolicy; /** Time of the checkin. If 0 or less, the time of the fsl_checkin_commit() call is used. */ double julianTime; /** If this is not NULL then the committed manifest will include a tag which closes the branch. The value of this string will be the value of the "closed" tag, and the value may be an empty string. The intention is that this gets set to a comment about why the branch is closed, but it is in no way mandatory. */ char const * closeBranch; /** Tells fsl_checkin_commit() to dump the generated manifest to this file. Intended only for debugging and testing. Checking in will fail if this file cannot be opened for writing. */ char const * dumpManifestFile; /* fossil(1) has many more options. We might want to wrap some of it up in the "incremental" state (f->ckin.mf). TODOs: A callback mechanism which supports the user cancelling the checkin. It is (potentially) needed for ops like confirming the commit of CRNL-only changes. 2021-03-09: we now have fsl_confirmer for this but currently no part of the checkin code needs a prompt. */ }; /** Empty-initialized fsl_checkin_opt instance, intended for use in const-copy constructing. */ #define fsl_checkin_opt_empty_m { \ NULL/*message*/, \ NULL/*messageMimeType*/, \ NULL/*user*/, \ NULL/*branch*/, \ NULL/*bgColor*/, \ false/*isPrivate*/, \ true/*calcRCard*/, \ false/*integrate*/, \ false/*allowMergeConflict*/,\ true/*scanForChanges*/,\ false/*rmRemovedFiles*/,\ 0/*deltaPolicy*/, \ 0.0/*julianTime*/, \ NULL/*closeBranch*/, \ NULL/*dumpManifestFile*/ \ } /** Empty-initialized fsl_checkin_opt instance, intended for use in copy-constructing. It is important that clients copy this value (or fsl_checkin_opt_empty_m) to cleanly initialize their fsl_checkin_opt instances, as this may set default values which (e.g.) a memset() would not. */ FSL_EXPORT const fsl_checkin_opt fsl_checkin_opt_empty; /** This creates and saves a "checkin manifest" for the current checkout. Its primary inputs is a list of files to commit. This list is provided by the client by calling fsl_checkin_enqueue() one or more times. If no files are explicitely selected (enqueued) then it calculates which local files have changed vs the current checkout and selects all of those. Non-file inputs are provided via the opt parameter. On success, it returns 0 and... - If newRid is not NULL, it is assigned the new checkin's RID value. - If newUuid is not NULL, it is assigned the new checkin's UUID value. Ownership of the bytes is passed to the caller, who must eventually pass them to fsl_free() to free them. Note that the new RID and UUID can also be fetched afterwards by calling fsl_ckout_version_info(). On error non-0 is returned and f's error state may (depending on the nature of the problem) contain details about the problem. Note, however, that any error codes returned here may have arrived from several layers down in the internals, and may not have a single specific interpretation here. When possible/practical, f's error state gets updated with a human-readable description of the problem. ACHTUNG: all pending checking state is cleaned if this function fails for any reason other than basic argument validation. This means any queued files or tags need to be re-applied if the client wants to try again. That is somewhat of a bummer, but this behaviour is the only way we can ensure that then the pending checkin state does not get garbled on a second use. When in doubt about the state, the client should call fsl_checkin_discard() to clear it before try to re-commit. (Potential TODO: add a success/fail state flag to the checkin state and only clean up on success? OTOH, since something in the state likely caused the problem, we might not want to do that.) This operation does all of its db-related work in a transaction, so it rolls back any db changes if it fails. To implement a "dry-run" mode, simply wrap this call in a transaction started on the fsl_cx_db_ckout() db handle (passing it to fsl_db_transaction_begin()), then, after this call, either cal; fsl_db_transaction_rollback() (to implement dry-run mode) or fsl_db_transaction_commit() (for "wet-run" mode). If this function returns non-0 due to anything more serious than basic argument validation, such a transaction will be in a roll-back state. Some of the more notable, potentially not obvious, error conditions: - Trying to commit against a closed leaf: FSL_RC_ACCESS. Doing so is not permitted by fossil(1), so we disallow it here. - An empty/NULL user name or commit message, or no files were selected which actually changed: FSL_RC_MISSING_INFO. In these cases f's error state describes the problem. - Some resource is not found (e.g. an expected RID/UUID could not be resolved): FSL_RC_NOT_FOUND. This would generally indicate some sort of data consistency problem. i.e. it's quite possibly very bad if this is returned. - If the checkin would result in no file-level changes vis-a-vis the current checkout, FSL_RC_NOOP is returned. BUGS: - It cannot currently properly distinguish a "no-op" commit, one in which no files were modified or only their permissions were modifed. @see fsl_checkin_enqueue() @see fsl_checkin_dequeue() @see fsl_checkin_discard() @see fsl_checkin_T_add() */ FSL_EXPORT int fsl_checkin_commit(fsl_cx * f, fsl_checkin_opt const * opt, fsl_id_t * newRid, fsl_uuid_str * newUuid); /** Works like fsl_deck_T_add(), adding the given tag information to the pending checkin state. Returns 0 on success, non-0 on error. A checkin may, in principal, have any number of tags, and this may be called any number of times to add new tags to the pending commit. This list of tags gets cleared by a successful fsl_checkin_commit() or by fsl_checkin_discard(). Decks require that each tag be distinct from each other (none may compare equivalent), but that check is delayed until the deck is output into its final artifact form. @see fsl_checkin_enqueue() @see fsl_checkin_dequeue() @see fsl_checkin_commit() @see fsl_checkin_discard() @see fsl_checkin_T_add2() */ FSL_EXPORT int fsl_checkin_T_add( fsl_cx * f, fsl_tagtype_e tagType, fsl_uuid_cstr uuid, char const * name, char const * value); /** Works identically to fsl_checkin_T_add() except that it takes its argument in the form of a T-card object. On success ownership of t is passed to mf. On error (see fsl_deck_T_add()) ownership is not modified. Results are undefined if either argument is NULL or improperly initialized. */ FSL_EXPORT int fsl_checkin_T_add2( fsl_cx * f, fsl_card_T * t ); /** Clears all contents from f's checkout database, including the vfile table, vmerge table, and some of the vvar table. The tables are left intact. Returns 0 on success, non-0 if f has no checkout or for a database error. */ FSL_EXPORT int fsl_ckout_clear_db(fsl_cx *f); /** Returns the base name of the current platform's checkout database file. That is "_FOSSIL_" on Windows and ".fslckout" everywhere else. The returned bytes are static. TODO: an API which takes a dir name and looks for either name */ FSL_EXPORT char const *fsl_preferred_ckout_db_name(); /** File-overwrite policy values for use with fsl_ckup_opt and friends. */ enum fsl_file_overwrite_policy_e { /** Indicates that an error should be triggered if a file would be overwritten. */ FSL_OVERWRITE_ERROR = 0, /** Indicates that files should always be overwritten by */ FSL_OVERWRITE_ALWAYS, /** Indicates that files should never be overwritten, and silently skipped over. This is almost never what one wants to do. */ FSL_OVERWRITE_NEVER }; typedef enum fsl_file_overwrite_policy_e fsl_file_overwrite_policy_e; /** State values for use with fsl_ckup_state::fileRmInfo. */ enum fsl_ckup_rm_state_e { /** Indicates that the file was not removed in a given checkout. Guaranteed to have the value 0 so that it is treated as boolean false. No other entries in this enum have well-defined values. */ FSL_CKUP_RM_NOT = 0, /** Indicates that a file was removed from a checkout but kept in the filesystem because it was locally modified. */ FSL_CKUP_RM_KEPT, /** Indicates that a file was removed from a checkout and the filesystem, with the caveat that failed attempts to remove from the filesystem are ignored for Reasons but will be reported as if the unlink worked. */ FSL_CKUP_RM }; typedef enum fsl_ckup_rm_state_e fsl_ckup_rm_state_e; /** Under construction. Work in progress... Options for "opening" a fossil repository database. That is, creating a new fossil checkout database and populating its schema, _without_ checking out any files. (That latter part is up for reconsideration and this API might change in the future to check out files after creating/opening the db.) */ struct fsl_repo_open_ckout_opt { /** Name of the target directory, which must already exist. May be relative, e.g. ".". The repo-open operation will chdir to this directory for the duration of the operation. May be NULL, in which case the current directory is assumed and no chdir is performed. */ char const * targetDir; /** The filename, with no directory components, of the desired checkout db name. For the time being, always leave this NULL and let the library decide. It "might" (but probably won't) be interesting at some point to allow the client to specify a different name (noting that that would be directly incompatible with fossil(1)). */ char const * ckoutDbFile; /** Policy for how to handle overwrites of files extracted from a newly-opened checkout. Potential TODO: replace this with a fsl_confirmer, though that currently seems like overkill for this particular case. */ fsl_file_overwrite_policy_e fileOverwritePolicy; /** fsl_repo_open_ckout() installs the fossil checkout schema. If this is true it will forcibly replace any existing relevant schema components in the checkout db, otherwise it will fail when it tries to overwrite an existing schema and cannot. */ bool dbOverwritePolicy; /** Of true, the checkout-open process will look for an opened checkout in the target directory and its parents (recursively) and fail with FSL_RC_ALREADY_EXISTS if one is found. */ bool checkForOpenedCkout; }; typedef struct fsl_repo_open_ckout_opt fsl_repo_open_ckout_opt; /** Empty-initialized fsl_repo_open_ckout_opt const-copy constructer. */ #define fsl_repo_open_ckout_opt_m { \ NULL/*targetDir*/, NULL/*ckoutDbFile*/, \ FSL_OVERWRITE_ERROR/*fileOverwritePolicy*/, \ false/*dbOverwritePolicy*/, \ -1/*checkForOpenedCkout*/ \ } /** Empty-initialised fsl_repo_open_ckout_opt instance. Clients should copy this value (or fsl_repo_open_ckout_opt_empty_m) to initialise fsl_repo_open_ckout_opt instances for sane default values. */ FSL_EXPORT const fsl_repo_open_ckout_opt fsl_repo_open_ckout_opt_empty; /** Work in progress... Opens a checkout db for use with the currently-connected repository or creates a new one. If opening an existing one, it gets "stolen" from any repository it might have been previously mapped to. - Requires that f have an opened repository db and no opened checkout. Returns FSL_RC_NOT_A_REPO if no repo is opened and FSL_RC_MISUSE if a checkout *is* opened. - Creates/re-uses a .fslckout DB in the dir opt->targetDir. The directory must be NULL or already exist, else FSL_RC_NOT_FOUND is returned. If opt->dbOverwritePolicy is false then it fails with FSL_RC_ALREADY_EXISTS if that directory already contains a checkout db. Note that this does not extract any SCM'd files from the repository, it only opens (and possibly creates) the checkout database. Pending: - If opening an existing checkout db for a different repo then delete the STASH and UNDO entries, as they're not valid for a different repo. */ FSL_EXPORT int fsl_repo_open_ckout( fsl_cx * f, fsl_repo_open_ckout_opt const * opt ); typedef struct fsl_ckup_state fsl_ckup_state; /** A callback type for use with fsl_ckup_state. It gets called via fsl_repo_ckout() and fsl_ckout_update() to report progress of the extraction process. It gets called after one of those functions has successfully extracted a file or skipped over it because the file existed and the checkout options specified to leave existing files in place. It must return 0 on success, and non-0 will end the extraction process, propagating that result code back to the caller. If this callback fails, the checkout's contents may be left in an undefined state, with some files updated and others not. All database-side data will be consistent (the transaction is rolled back) but filesystem-side changes may not be. */ typedef int (*fsl_ckup_f)( fsl_ckup_state const * cState ); /** This enum lists the various types of individual file change states which can happen during a checkout, update, or merge. */ enum fsl_ckup_fchange_e { /** Sentinel value. */ FSL_CKUP_FCHANGE_INVALID = -1, /** Was unchanged between the previous and updated-to version, so no change was made to the on-disk file. This is the only entry in the enum which is guaranteed to have a specific value: 0, so that it can be used as a boolean false. */ FSL_CKUP_FCHANGE_NONE = 0, /** Added to SCM in the updated-to version. */ FSL_CKUP_FCHANGE_ADDED, /** Added to SCM in the current checkout version and carried over into the updated-to version. */ FSL_CKUP_FCHANGE_ADD_PROPAGATED, /** Removed from SCM in the updated-to to version OR in the checked-out version but not yet committed. a.k.a. it became "unmanaged." Do we need to differentiate between those cases? */ FSL_CKUP_FCHANGE_RM, /** Removed from the checked-out version but not yet commited, so was carried over to the updated-to version. */ FSL_CKUP_FCHANGE_RM_PROPAGATED, /** Updated or replaced without a merge by the checkout/update process. */ FSL_CKUP_FCHANGE_UPDATED, /** Merge was not performed because at least one of the inputs appears to be binary. The updated-to version overwrites the previous version in this case. */ FSL_CKUP_FCHANGE_UPDATED_BINARY, /** Updated with a merge by the update process. */ FSL_CKUP_FCHANGE_MERGED, /** Special case of FSL_CKUP_FCHANGE_MERGED. Merge was performed and conflicts were detected. The newly-updated file will contain conflict markers. @see fsl_buffer_contains_merge_marker() */ FSL_CKUP_FCHANGE_CONFLICT_MERGED, /** Added in the current checkout but also contained in the updated-to version. The local copy takes precedence. */ FSL_CKUP_FCHANGE_CONFLICT_ADDED, /** Added by the updated-to version but a local unmanaged copy exists. The local copy is overwritten, per historical fossil(1) convention (noting that fossil has undo support to allow one to avoid loss of such a file's contents). TODO: use confirmer here to ask user whether to overwrite. */ FSL_CKUP_FCHANGE_CONFLICT_ADDED_UNMANAGED, /** Edited locally but removed from updated-to version. Local edits will be left in the checkout tree. */ FSL_CKUP_FCHANGE_CONFLICT_RM, /** Cannot merge if one or both of the update/updating verions of a file is a symlink. The updated-to version overwrites the previous version in this case. We probably need a better name for this. */ FSL_CKUP_FCHANGE_CONFLICT_SYMLINK, /** Indicates that a merge of binary content was requested. TODO: figure out why UPDATE uses the target version, instead of triggering an error here, and why MERGE does not do the same. fossil(1) simply skips over, with a warning, binaries during a merge. */ FSL_CKUP_FCHANGE_CONFLICT_BINARY, /** File was renamed in the updated-to version. If a file is both modified and renamed, it is flagged as renamed instead of modified. */ FSL_CKUP_FCHANGE_RENAMED, /** Locally modified. This state appears only when "updating" a checkout to the same version. */ FSL_CKUP_FCHANGE_EDITED }; typedef enum fsl_ckup_fchange_e fsl_ckup_fchange_e; /** State to be passed to fsl_ckup_f() implementations via calls to fsl_repo_ckout() and fsl_ckout_update(). */ struct fsl_ckup_state { /** The core SCM state for the just-extracted file. Note that its content member will be NULL: the content is not passed on via this interface because it is only loaded for files which require overwriting. An update process may synthesize content for extractState->fCard which do not 100% reflect the file on disk. Of primary note here: 1) fCard->uuid will refer to the hash of the updated-to version, as opposed to the hash of the on-disk file (which may differ due to having local edits merged in). 2) For the update process, fCard->priorName will be NULL unless the file was renamed between the original and updated-to versions, in which case priorName will refer to the original version's name. */ fsl_repo_extract_state const * extractState; /** Optional client-dependent state for use in the fsl_ckup_f() callback. This is copies from the corresponding fsl_ckup_opt::callbackState member. */ void * callbackState; /** Vaguely describes the type of change the current call into the fsl_ckup_f() represents. The full range of values is not valid for all operations. Specifically: Checkout only uses: FSL_CKUP_FCHANGE_NONE FSL_CKUP_FCHANGE_UPDATED FSL_CKUP_FCHANGE_RM For update operations all (or most) values are potentially possible. If this has a value of FSL_CKUP_FCHANGE_RM, this->fileRmInfo will provide a bit more detail. */ fsl_ckup_fchange_e fileChangeType; /** Indicates whether the file was removed by the process: - FSL_CKUP_RM_NOT = Was not removed. - FSL_CKUP_RM_KEPT = Was removed from the checked-out version but left in the filesystem because the confirmer said to. - FSL_CKUP_RM = Was removed from the checkout and the filesystem. When this->dryRun is true, this specifies whether the file would have been removed. */ fsl_ckup_rm_state_e fileRmInfo; /** If fsl_repo_ckout()'s or fsl_ckout_update()'s options specified that the mtime should be set on each updated file, this holds that time. If the file existed and was not overwritten, it is set to that file's time. Else it is set to the current time (which may differ by a small fraction of a second from the file-write time because we avoid stat()'ing it again after writing). If this->fileRmInfo indicates that a file was removed, this might (depending on availability of the file in the filesystem at the time) be set to 0. When running in dry-run mode, this value may be 0, as we may not have a file in place which we can stat() to get it, nor a db entry from which to fetch it. This option is ignored for merge operations. */ fsl_time_t mtime; /** The size of the extracted file, in bytes. If the file was removed from the filesystem (or removal was at least attempted) then this is set to -1. */ fsl_int_t size; /** True if the current checkout/update is running in dry-run mode, else false. See fsl_ckup_opt::dryRun for details. */ bool dryRun; }; /** Options for use with fsl_repo_ckout() and fsl_ckout_update(). i.e. for checkout and update. */ struct fsl_ckup_opt { /** The version of the repostitory to check out or update. This must be the blob.rid of a checkin artifact. */ fsl_id_t checkinRid; /** Gets called once per checked-out or updated file, passed a fsl_ckup_state instance with information about the checked-out file and related metadata. May be NULL. */ fsl_ckup_f callback; /** State to be passed to this->callback via the fsl_ckup_state::callbackState member. */ void * callbackState; /** An optional "confirmer" for answering questions about file overwrites and deletions posed by the checkout process. By default this confirmer of the associated fsl_cx instance is used. Caveats: - This is not currently used by the update process, only checkout. - If this->setMTime is true, the mtime is NOT set for any files which already exist and are skipped due to the confirmer saying to leave them in place. - Similarly, if the confirmer says to never overwrite files, permissions on existing files are not modified. fsl_repo_ckout() does not (re)write unmodified files, and thus may leave such files with different permissions. That's on the to-fix list. */ fsl_confirmer confirmer; /** If true, the checkout/update processes will calculate the (synthetic) mtime of each extracted file and set its mtime. This is a relatively expensive operation which calculates the "effective mtime" of each file by calculating it: Fossil does not record file timestamps, instead treating files as if they had the timestamp of the most recent checkin in which they were added or modified. It's generally a good idea to let the update process stamp the _current_ time on modified files, in order to avoid any hiccups with build processes which rely on accurate times (e.g. Makefiles). When doing a clean checkout, it's often interesting to see the "original" times, though. */ bool setMtime; /** A hint to fsl_repo_ckout() and fsl_ckout_update() about whether it needs to scan the checkout for changes. Set this to false ONLY if the calling code calls fsl_ckout_changes_scan() (or equivalent, e.g. fsl_vfile_changes_scan()) immediately before calling fsl_repo_ckout() or fsl_ckout_update(), as those require a non-stale changes scan in order to function properly. */ bool scanForChanges; /** If true, the extraction process will "go through the motions" but will not write any files to disk. It will perform I/O such as stat()'ing to see, e.g., if it would have needed to overwrite a file. */ bool dryRun; }; typedef struct fsl_ckup_opt fsl_ckup_opt; /** Empty-initialized fsl_ckup_opt const-copy constructor. */ #define fsl_ckup_opt_m {\ -1/*checkinRid*/, NULL/*callback*/, NULL/*callbackState*/, \ fsl_confirmer_empty_m/*confirmer*/,\ false/*setMtime*/, true/*scanForChanges*/,false/*dryRun*/ \ } /** Empty-initialised fsl_ckup_opt instance. Clients should copy this value (or fsl_ckup_opt_empty_m) to initialise fsl_ckup_opt instances for sane default values. */ FSL_EXPORT const fsl_ckup_opt fsl_ckup_opt_empty; /** A fsl_repo_extract() proxy which extracts the contents of the repository version specified by opt->checkinRid to the root directory of f's currently-opened checkout. i.e. it performs a "checkout" operation. For each extracted entry, cOpt->callback (if not NULL) will be passed a (fsl_ckup_state const*) which contains a pointer to the fsl_repo_extract_state and some additional metadata regarding the extraction. The value of cOpt->callbackState will be set as the callbackState member of that fsl_ckup_state struct, so that the client has a way of passing around app-specific state to that callback. After successful completion, the process will report (see below) any files which were part of the previous checkout version but are not part of the current version, optionally removing them from the filesystem (depending on the value of opt->rmMissingPolicy). It will IGNORE ANY DELETION FAILURE of files it attempts to delete. The reason it does not fail on removal error is because doing so would require rolling back the transaction, effectively undoing the checkout, but it cannot roll back any prior deletions which succeeded. Similarly, after all file removal is complete, it attempts to remove any now-empty directories left over by that process, also silently ignoring any errors. If the cOpt->dryRun option is specified, it will "go through the motions" of removing files but will not actually attempt filesystem removal. For purposes of the callback, however, it will report deletions as having happened (but will also set the dryRun flag on the object passed to the callback). After unpacking the SCM-side files, it may write out one or more manifest files, as described for fsl_ckout_manifest_write(), if the 'manifest' config setting says to do so. As part of the file-removal process, AFTER all "existing" files are processed, it calls cOpt->callback() (if not NULL) for each removed file, noting the following peculiarities in the fsl_ckup_state object which is passed to it for those calls: - It is called after the processing of "existing" files. Thus the file names passed during this step may appear "out of order" with regards to the others (which are guaranteed to be passed in lexical order, though whether it is case-sensitive or not depends on the repository's case-sensitivity setting). - fileRmInfo will indicate that the file was removed from the checkout, and whether it was actually removed or retained in the filesystem. This will indicate filesystem-level removal even when in dry-run mode, though in that case no filesystem-level removal is actually attempted. - extractState->fileRid will refer to the file's blob's RID for the previous checkout version. - extractState->content will be NULL. - extractState->callbackState will be NULL. - extractState->fCard will refer to the pre-removal state of the file. i.e. the state as it was in the checkout prior to this function being called. Returns 0 on success. Returns FSL_RC_NOT_A_REPO if f has no opened repo, FSL_RC_NOT_A_CKOUT if no checkout is opened. If cOpt->callback is not NULL and returns a non-0 result code, extraction ends and that result is returned. If it returns non-0 at any point after basic argument validation, it rolls back all changes or sets the current transaction stack into a rollback state. @see fsl_repo_ckout_open() */ FSL_EXPORT int fsl_repo_ckout(fsl_cx * f, fsl_ckup_opt const * cOpt); /** UNDER CONSTRUCTION. Performs an "update" operation on f's currenly-opened checkout. Performing an update is similar to performing a checkout, the primary difference being that an update will merge local file modifications into any newly-updated files, whereas a checkout will overwrite them. TODO?: fossil(1)'s update permits a list of files, in which case it behaves differently: it updates the given files to the version requested but leaves the checkout at its current version. To be able to implement that we either need clients to call this in a loop, changing opt->filename on each call (like how we do fsl_ckout_manage()) or we need a way for them to pass on the list of files/dir in the opt object. @see fsl_repo_ckout(). */ FSL_EXPORT int fsl_ckout_update(fsl_cx * f, fsl_ckup_opt const *opt); /** Tries to calculate a version to update the current checkout version to, preferring the tip of the current checkout's branch. On success, 0 is returned and *outRid is set to the calculated RID, which may be 0, indicating that no errors were encountered but no version could be calculated. On error, non-0 is returned, outRid is not modified, and f's error state is updated. Returns FSL_RC_NOT_A_CKOUT if f has no checkout opened and FSL_RC_NOT_A_REPO if no repo is opened. If it calculates that there are multiple viable descendants it returns FSL_RC_AMBIGUOUS and f's error state will contain a list of the UUIDs (or UUID prefixes) of those descendants. Sidebar: to get the absolute latest version, irrespective of the branch, use fsl_sym_to_rid() to resolve the symbolic name "tip". */ FSL_EXPORT int fsl_ckout_calc_update_version(fsl_cx * f, fsl_id_t * outRid); /** Bitmask used by fsl_ckout_manifest_setting() and fsl_ckout_manifest_write(). */ enum fsl_cx_manifest_mask_e { /** Coresponds to the file "manifest". */ FSL_MANIFEST_MAIN = 0x001, /** Coresponds to the file "manifest.uuid". */ FSL_MANIFEST_UUID = 0x010, /** Coresponds to the file "manifest.tags". */ FSL_MANIFEST_TAGS = 0x100 }; typedef enum fsl_cx_manifest_mask_e fsl_cx_manifest_mask_e; /** Returns a bitmask representing which manifest files, if any, will be written when opening or updating a checkout directory, as specified by the repository's 'manifest' configuration setting, and sets *m to a bitmask indicating which of those are enabled. It first checks for a versioned setting then, if no versioned setting is found, a repository-level setting. A truthy setting value (1, "on", "true") means to write the manifest and manifest.uuid files. A string with any of the letters 'r', 'u', or 't' means to write the [r]aw, [u]uid, and/or [t]ags file(s), respectively. If the manifest setting is falsy or not set, *m is set to 0, else *m is set to a bitmask representing which file(s) are considered to be auto-generated for this repository: - FSL_MANIFEST_MAIN = manifest - FSL_MANIFEST_UUID = manifest.uuid - FSL_MANIFEST_TAGS = manifest.tags Any db-related or allocation errors while trying to fetch the setting are silently ignored. For performance's sake, since this is potentially called often from fsl_reserved_fn_check(), this setting is currently cached by this routine (in the fsl_cx object), but that ignores the fact that the manifest setting can be modified at any time, either in a versioned setting file or the repository db, and may be modified from outside the library. There's a tiny back-door for working around that: if m is NULL, the cache will be flushed and no other work will be performed. Thus the following approach can be used to force a fresh check for that setting: ``` fsl_ckout_manifest_setting(f, NULL); // clears caches, does nothing else fsl_ckout_manifest_setting(f, &myInt); // loads/caches the setting ``` */ FSL_EXPORT void fsl_ckout_manifest_setting(fsl_cx *f, int *m); /** Might write out the files manifest, manifest.uuid, and/or manifest.tags for the current checkout to the the checkout's root directory. The 2nd-4th arguments are interpreted as follows: 0: Do not write that file. >0: Always write that file. <0: Use the value of the "manifest" config setting (see fsl_ckout_manifest_setting()) to determine whether or not to write that file. As each file is written, its mtime is set to that of the checkout version. (Forewarning: that behaviour may change if it proves to be problematic vis a vis build processes.) Returns 0 on success, non-0 on error: - FSL_RC_NOT_A_CKOUT if no checkout is opened. - FSL_RC_RANGE if the current checkout RID is 0 (indicating a fresh, empty repository). - Various potential DB/IO-related error codes. If the final argument is not NULL then it will be updated to contain a bitmask representing which files, if any, were written: see fsl_ckout_manifest_setting() for the values. It is updated regardless of success or failure and will indicate which file(s) was/were written before the error was triggered. Each file implied by the various manifest settings which is NOT written by this routine and is also not part of the current checkout (i.e. not listed in the vfile table) will be removed from disk, but a failure while attempting to do so will be silently ignored. @see fsl_repo_manifest_write() */ FSL_EXPORT int fsl_ckout_manifest_write(fsl_cx * const f, int manifest, int manifestUuid, int manifestTags, int * const wroteWhat ); /** Returns true if f has an opened checkout and the given absolute path is rooted in that checkout, else false. As a special case, it returns false if the path _is_ the checkout root unless zAbsPath has a trailing slash. (The checkout root is always stored with a trailing slash because that simplifies its internal usage.) Note that this is strictly a string comparison, not a filesystem-level operation. */ FSL_EXPORT bool fsl_is_rooted_in_ckout(fsl_cx * const f, char const * const zAbsPath); /** Works like fsl_is_rooted_in_ckout() except that it returns 0 on success, and on error updates f with a description of the problem and returns non-0: FSL_RC_RANGE or (if updating the error state fails) FSL_RC_OOM. */ FSL_EXPORT int fsl_is_rooted_in_ckout2(fsl_cx * const f, char const * const zAbsPath); /** Change-type values for use with fsl_ckout_revert_f() callbacks. */ enum fsl_ckout_revert_e { /** Sentinel value. */ FSL_REVERT_NONE = 0, /** File was previously queued for addition but unqueued by the revert process. */ FSL_REVERT_UNMANAGE, /** File was previously queued for removal but unqueued by the revert process. If the file's contents or permissions were also reverted then the file is reported as FSL_REVERT_PERMISSIONS or FSL_REVERT_CONTENTS instead. */ FSL_REVERT_REMOVE, /** File was previously scheduled to be renamed, but the rename was reverted. The name reported to the callback is the original one. If a file was both modified and renamed, it will be flagged as renamed instead of modified, for consistency with the usage of fsl_ckup_fchange_e's FSL_CKUP_FCHANGE_RENAMED. FIXME: this does not mean that the file on disk was actually renamed (if needed). That is TODO, pending addition of code to perform renames. */ FSL_REVERT_RENAME, /** File's permissions (only) were reverted. */ FSL_REVERT_PERMISSIONS, /** File's contents reverted. This value trumps any others in this enum. Thus if a file's permissions and contents were reverted, or it was un-renamed and its contents reverted, it will be reported using this enum entry. */ FSL_REVERT_CONTENTS }; typedef enum fsl_ckout_revert_e fsl_ckout_revert_e; /** Callback type for use with fsl_ckout_revert(). For each reverted file it gets passed the checkout-relative filename, type of change, and the callback state pointer which was passed to fsl_ckout_revert(). If it returns non-0, the revert process will end in an error and that code will be propagated back to the caller. In such cases, any files reverted up until that point will still be reverted on disk but the reversion in the database will be rolled back. A change scan (e.g. fsl_ckout_changes_scan()) will restore balance to that equation, but these callbacks should only return non-0 in for catastrophic failure. */ typedef int (*fsl_ckout_revert_f)( char const *zFilename, fsl_ckout_revert_e changeType, void * callbackState ); /** Options for passing to fsl_ckout_revert(). */ struct fsl_ckout_revert_opt { /** File or directory name to revert. See also this->vfileIds. */ char const * filename; /** An alternative to assigning this->filename is to point this->vfileIds to a bag of vfile.id values. If this member is not NULL, fsl_ckout_revert() will ignore this->filename. @see fsl_filename_to_vfile_ids() */ fsl_id_bag const * vfileIds; /** Interpret filename as relative to cwd if true, else relative to the current checkout root. This is ignored when this->vfileIds is not NULL. */ bool relativeToCwd; /** If true, fsl_vfile_changes_scan() is called to ensure that the filesystem and vfile tables agree. If the client code has called that function, or its equivalent, since any changes were made to the checkout then this may be set to false to speed up the revert process. */ bool scanForChanges; /** Optional callback to notify the client of what gets reverted. */ fsl_ckout_revert_f callback; /** State for this->callback. */ void * callbackState; }; typedef struct fsl_ckout_revert_opt fsl_ckout_revert_opt; /** Initialized-with-defaults fsl_ckout_revert_opt instance, intended for use in const-copy initialization. */ #define fsl_ckout_revert_opt_empty_m { \ NULL/*filename*/,NULL/*vfileIds*/,true/*relativeToCwd*/,true/*scanForChanges*/, \ NULL/*callback*/,NULL/*callbackState*/ \ } /** Initialized-with-defaults fsl_ckout_revert_opt instance, intended for use in non-const copy initialization. */ FSL_EXPORT const fsl_ckout_revert_opt fsl_ckout_revert_opt_empty; /** Reverts changes to checked-out files, replacing their on-disk versions with the current checkout's version. If zFilename refers to a directory, all managed files under that directory are reverted (if modified). If zFilename is NULL or empty, all modifications in the current checkout are reverted. If a file has been added but not yet committed, the add is un-queued but the file is otherwise untouched. If the file has been queued for removal, this removes it from that queue as well as restores its contents. If a rename is pending for the given filename, the name may match either its original name or new name. Whether or not that will actually work when file A is renamed to B and file C is renamed to A is anyone's guess. (Noting that (fossil mv) won't allow that situation to exist in the vfile table for a single checkout version, so it seems safe enough.) If the 4th argument is not NULL then it is called for each revert (_after_ the revert happens) to report what was done with that file. It gets passed the checkout-relative name of each reverted file. The 5th argument is not interpreted by this function but is passed on as-is as the final argument to the callback. If the callback returns non-0, the revert process is cancelled, any pending transaction is set to roll back, and that error code is returned. Note that cancelling a revert mid-process will leave file changes made by the revert so far in place, and thus the checkout db and filesystem will be in an inconsistent state until fsl_vfile_changes_scan() (or equivalent) is called to restore balance to the world. Files which are not actually reverted because their contents or permissions were not modified on disk are not reported to the callback unless the reversion was the un-queuing of an ADD or REMOVE operation. Returns 0 on success, any number of non-0 results on error. */ FSL_EXPORT int fsl_ckout_revert( fsl_cx * const f, fsl_ckout_revert_opt const * opt ); /** Expects f to have an opened checkout and zName to be the name of an entry in the vfile table where vfile.vid == vid. If vid<=0 then the current checkout RID is used. This function does not do any path resolution or normalization on zName and checks only for an exact match (honoring f's case-sensitivity setting - see fsl_cx_case_sensitive_set()). On success it returns 0 and assigns *vfid to the vfile.id value of the matching file. If no match is found, 0 is returned and *vfile is set to 0. Returns FSL_RC_NOT_A_CKOUT if no checkout is opened, FSL_RC_RANGE if zName is not a simple path (see fsl_is_simple_pathname()), and any number of codes for db-related errors. This function matches only vfile.pathname, not vfile.origname, because it is possible for a given name to be in both fields (in different records) at the same time. */ FSL_EXPORT int fsl_filename_to_vfile_id( fsl_cx * f, fsl_id_t vid, char const * zName, fsl_id_t * vfid ); /** Searches the `vfile` table where `vfile.vid=vid` for a name which matches `zName` or all `vfile` entries found under a subdirectory named `zName` (with no trailing slash). `zName` must be relative to the checkout root. As a special case, if `zName` is `NULL`, empty, or `"."` then all files in `vfile` with the given `vid` are selected. For each entry it finds, it adds the `vfile.id` to `dest`. If `vid<=0` then the current checkout RID is used. If `changedOnly` is `true` then only entries which have been marked in the `vfile` table as having some sort of change are included, so if `true` then fsl_ckout_changes_scan() (or equivalent) must have been "recently" called to ensure that state is up to do. This routine only checks the `vfile` table for "is changed" state, it does not do filesystem-level checks on the files. This search honors the context-level case-sensitivity setting (see fsl_cx_case_sensitive_set()). Returns 0 on success. Not finding anything is not treated as an error, though we could arguably return `FSL_RC_NOT_FOUND` for the cases which use this function. In order to determine whether or not any results were actually found, compare `dest->entryCount` before and after calling this. This function matches only `vfile.pathname`, not `vfile.origname`, because it is possible for a given name to be in both fields (in different records) at the same time. @see fsl_ckout_vfile_ids() */ FSL_EXPORT int fsl_filename_to_vfile_ids( fsl_cx * const f, fsl_id_t vid, fsl_id_bag * const dest, char const * zName, bool changedOnly); /** This is a variant of fsl_filename_to_vfile_ids() which accepts filenames in a more flexible form than that routine. This routine works exactly like that one except for the following differences: 1) The given filename and the relativeToCwd arguments are passed to by fsl_ckout_filename_check() to canonicalize the name and ensure that it points to someplace within f's current checkout. 2) Because of (1), zName may not be NULL or empty. To fetch all of the vfile IDs for the current checkout, pass a zName of "." and relativeToCwd=false. Returns 0 on success, FSL_RC_MISUSE if zName is NULL or empty, FSL_RC_OOM on allocation error, FSL_RC_NOT_A_CKOUT if f has no opened checkout. */ FSL_EXPORT int fsl_ckout_vfile_ids( fsl_cx * const f, fsl_id_t vid, fsl_id_bag * const dest, char const * zName, bool relativeToCwd, bool changedOnly ); /** This "mostly internal" routine (re)populates f's checkout vfile table with all files from the given checkin manifest. If manifestRid is 0 or less then the current checkout's RID is used. If vfile already contains any content for the given checkin, it is left intact (and several processes rely on that behavior to keep it from nuking, e.g., as-yet-uncommitted queued add/rm entries). Returns 0 on success, any number of codes on any of many potential errors. f must not be NULL and must have opened checkout and repository databases. In debug builds it will assert that that is so. If the 3rd argument is true, any entries in vfile for checkin versions other than the one specified in the 2nd argument are cleared from the vfile table. That is _almost_ always the desired behavior, but there are rare cases where vfile needs to temporarily (for the duration of a single transaction) hold state for multiple versions. If the 4th argument is not NULL, it gets assigned the number of blobs from the given version which are currently missing from the repository due to being phantoms (as opposed to being shunned). Returns 0 on success, FSL_RC_NOT_A_CKOUT if no checkout is opened, FSL_RC_OOM on allocation error, FSL_RC_DB for db-related problems, et.al. Misc. notes: - This does NOT update the "checkout" vvar table entry because this routine is sometimes used in contexts where we need to briefly maintain two vfile versions and keep the previous checkout version. - Apps must take care to not leave more than one version in the vfile table for longer than absolutely needed. They "really should" use fsl_vfile_unload() to clear out any version they load with this routine. @see fsl_vfile_unload() @see fsl_vfile_unload_except() */ FSL_EXPORT int fsl_vfile_load(fsl_cx * const f, fsl_id_t manifestRid, bool clearOtherVersions, uint32_t * missingCount); /** Clears out all entries in the current checkout's vfile table with the given vfile.vid value. If vid<=0 then the current checkout RID is used (which is never a good idea from client-side code!). ACHTUNG: never do this without understanding the consequences. It can ruin the current checkout state. Returns 0 on success, FSL_RC_NOT_A_CKOUT if f has no checkout opened, FSL_RC_DB on any sort of db-related error (in which case f's error state is updated with a description of the problem). @see fsl_vfile_load() @see fsl_vfile_unload_except() */ FSL_EXPORT int fsl_vfile_unload(fsl_cx * const f, fsl_id_t vid); /** A counterpart of fsl_vfile_unload() which removes all vfile entries where vfile.vid is not the given vid. If vid is <=0 then the current checkout RID is used. Returns 0 on success, FSL_RC_NOT_A_CKOUT if f has no checkout opened, FSL_RC_DB on any sort of db-related error (in which case f's error state is updated with a description of the problem). @see fsl_vfile_load() @see fsl_vfile_unload() */ FSL_EXPORT int fsl_vfile_unload_except(fsl_cx * const f, fsl_id_t vid); /** Performs a "fingerprint check" between f's current checkout and repository databases. Returns 0 if either there is no checkout, no mismatch, or it is impossible to determine because the checkout is missing a fingerprint (which is legal for "older" checkout databases). If a mismatch is found, FSL_RC_REPO_MISMATCH is returned. Returns some other non-0 code on a lower-level error (db access, OOM, etc.). A mismatch can happen when the repository to which a checkout belongs is replaced, either with a completely different repository or a copy/clone of that same repository. Each repository copy may have differing blob.rid values, and those are what the checkout database uses to refer to repository-side data. If those RIDs change, then the checkout is left pointing to data other than what it should be. TODO: currently the library offers no automated recovery mechanism from a mismatch, the only remedy being to close the checkout database, destroy it, and re-create it. fossil(1) is able, in some cases, to automatically recover from this situation. */ FSL_EXPORT int fsl_ckout_fingerprint_check(fsl_cx * f); /** Looks for the given file in f's current checkout. If relativeToCwd then the name is resolved from the current directory, otherwise it is assumed to be relative to the checkout root or an absolute path with the checkout dir as a prefix of that path. On success, 0 is returned and dest's gets populated with the content of the file. On error, non-0 is returned and, depending on the error type, dest might be partially populated. f's error state will be updated to describe the error. Results are undefined if any pointer argument is NULL. This function currently resolves symlinks on its way to the content, but that behaviour may change in the future to reflect f's symlink preferences. */ FSL_EXPORT int fsl_ckout_file_content(fsl_cx * const f, bool relativeToCwd, char const * zName, fsl_buffer * const dest); /** Fetches the timestamp of the given F-card's name against the filesystem and/or the most recent checkin in which it was modified (as reported by fsl_mtime_of_manifest()). vid is the checkin version to look at. If it's <=0, the current checkout will be used. On success, returns 0 and: - If repoMtime is not NULL then (*repoMtime) is assigned to the result of fsl_mtime_of_manifest_file() for the given file. - If localMtime is not NULL then (*localMtime) is assigned to the checkout-local timestamp of the file. Returns non-0 on error. Some of the potential results include: - FSL_RC_NOT_A_CKOUT. - FSL_RC_NOT_FOUND if the filename cannot be resolved in the requested version or cannot be stat()'d. - FSL_RC_OOM. */ FSL_EXPORT int fsl_card_F_ckout_mtime(fsl_cx * const f, fsl_id_t vid, fsl_card_F const * const fc, fsl_time_t * repoMtime, fsl_time_t * localMtime); /** File change types for use with fsl_merge_state::fileChangeType. Terminology used in some of the descriptions: - (P) is the "pivot" - the common ancestor for the merge. - (M) is the version being merged in to... - (V) is current checkout version into which (M) is being merged. Maintenance reminder: this enum's values must start at 0 and increment sequentially, and FSL_MERGE_FCHANGE_count must be the final entry. This is to enable the creation of arrays, e.g. for keeping track (client-side) of how many times a given change type has been seen during a given merge run. */ enum fsl_merge_fchange_e { /** Not currently used. Merge does not (and cannot without some surgery) report state of files unaffected by a merge. This entry exists for the case that that changes. This is the only entry in the enum which is guaranteed to have a specific value: 0, so that it can be used as a boolean false. */ FSL_MERGE_FCHANGE_NONE = 0, /** File was added to (V) from (M). */ FSL_MERGE_FCHANGE_ADDED, /** File content was copied as-is from (M) to (V). */ FSL_MERGE_FCHANGE_COPIED, /** File was removed from (V) via (M). a.k.a. it became "unmanaged." */ FSL_MERGE_FCHANGE_RM, /** Content from (M) was merged into (V). */ FSL_MERGE_FCHANGE_MERGED, /** Special case of FSL_MERGE_FCHANGE_MERGED. Merge was performed from (M) to (V) and conflicts were detected. The newly-updated file will contain conflict markers. @see fsl_buffer_contains_merge_marker() */ FSL_MERGE_FCHANGE_CONFLICT_MERGED, #if 0 /** Added in the current checkout but also contained in the updated-to version. The local copy takes precedence. */ FSL_MERGE_FCHANGE_CONFLICT_ADDED, #endif /** Added to (V) by (M) but a local unmanaged copy exists. The local copy is overwritten, per historical fossil(1) convention (noting that fossil has undo support to allow one to avoid loss of such a file's contents). */ FSL_MERGE_FCHANGE_CONFLICT_ADDED_UNMANAGED, #if 0 /* Fossil deletes merged-over removed files, regardless of whether they're locally edited, so we'll do the same for now (noting that fossil has undo support which can hypothetically save that case from data loss). fsl_ckout_update() does not do so, but has extra infastructure to deal with this. */ /** Edited locally but removed from merged-in version. Local edits will be left in the checkout tree. */ FSL_MERGE_FCHANGE_CONFLICT_RM, #endif /** Cannot merge if one or both of the update/updating versions of a file is a symlink. This case needs re-thinking. fossil(1) simply skips such merges with a warning but no error. This library has no warning mechanism other than to pass this code on to the fsl_merge_f() callback, so that's what we do. For UPDATE ops, the updated-to version overwrites the previous version in this case. Why merge doesn't do that isn't clear, but it's probably because we can have any number of merge parents and choosing which one to use in the merge/replace case would be impossible. */ FSL_MERGE_FCHANGE_CONFLICT_SYMLINK, /** Indicates that a merge of binary content was requested. We cannot merge binaries, so this indicates that the file in question was skipped over for merge purposes. fossil(1) simply skips over, with a warning, binaries during a merge, so we do the same (for lack of a better option). */ FSL_MERGE_FCHANGE_CONFLICT_BINARY, /** Indicates that the given file cannot be merged because no common ancestor can be found for it. This condition is not strictly fatal but does indicate a problem with the input data. Merging will continue unless the fsl_merge_f() to which this is reported returns non-0 to cancel it. This particular status is reported very early in the fsl_ckout_merge() process, before any files have been written by the merge, thus the callback can effectively abort the merge without side-effects by returning non-0 if this status is reported to it. If the fsl_ckout_merge() call has no callback set, this case will go silently undiagnosed! Potential TODO: add a flag to fsl_merge_opt to tell it to treat any of these cases as merge-fatal. */ FSL_MERGE_FCHANGE_CONFLICT_ANCESTOR, /** File was renamed in the updated-to version. If a file is both modified and renamed, it will be reported twice: once for each type of change. The new name is reported via fsl_merge_state::filename and the previous name via fsl_merge_state::priorName. */ FSL_MERGE_FCHANGE_RENAMED, /** This is the number of entries in this enum, for purposes of creating, e.g. arrays of counters. This entry is never reported via a fsl_merge_state::fileChangeType. */ FSL_MERGE_FCHANGE_count }; typedef enum fsl_merge_fchange_e fsl_merge_fchange_e; /** Reqired forward decl. */ typedef struct fsl_merge_opt fsl_merge_opt; /** UNDER CONSTRUCTION! INCOMPLETE! A type for passing state to fsl_merge_f callbacks during fsl_ckout_merge() processing. For each step of a merge operation which affects a file, state reflecting that change is set in one of these objects and it is passed to the fsl_merge_f implementation supplied by the caller. Unlike fsl_ckout_update(), fsl_ckout_merge() reports only files which are affected by a merge, not unmodified files. Whether that's a bug or a feature is not yet clear, but the merge algorithm does not, as is, support reporting unaffected files. Due to the complexity and intricacy of the merge operation, it is possible that any given file will get passed to the fsl_merge_f() callback more than once with a different fsl_merge_state::fileChangeType value. Most notably, a file which has been modified and renamed may be passed on one with FSL_MERGE_FCHANGE_COPIED and once with FSL_MERGE_FCHANGE_RENAMED. Whether that's a bug or a feature is as-yet undecided, but (A) fossil(1) does it that way and (B) _not_ doing that would require some rearchitecting. */ struct fsl_merge_state { /** The fsl_cx object for which the current merge is running. */ fsl_cx * f; /** The options object which drives the current fsl_ckout_merge() run. */ fsl_merge_opt const * opt; /** The checkout-relative name of the file affected by the merge. These bytes are invalidated after the fsl_merge_f() callback returns, so must be copied if the client requires them for later. */ char const * filename; /** If this->fileChangeType is FSL_MERGE_FCHANGE_RENAMED then this is the previous name of the file, else it is NULL. */ char const * priorName; /** Indicates the state of the file currently being merged. Merge does not support the full range fo fsl_ckup_fchange_e values. TODO: list which it does or create a new enum which enumerates only merge-specific change types. */ fsl_merge_fchange_e fileChangeType; /** Indicates whether the current file was removed. A state of FSL_CKUP_RM_KEPT and fileChangeType of FSL_MERGE_FCHANGE_RM indicates that the file has become unmanaged but the local copy was retained because it was flagged as locally modified. */ fsl_ckup_rm_state_e fileRmInfo; }; typedef struct fsl_merge_state fsl_merge_state; /** Callback type for use with fsl_merge_opt and fsl_ckout_merge(). */ typedef int (*fsl_merge_f)(fsl_merge_state const * const); /** Merge type enum for use with fsl_merge_opt::mergeType. The values of these entries are fossil-magic values for the `vmerge.id` db field and must stay in sync with fossil's definition. */ enum fsl_merge_type_e { /** Indicates a normal merge. */ FSL_MERGE_TYPE_NORMAL = 0, /** Indicates an "integrate" merge, which tells the next checkin operation to apply a "closed" tag to the checkin from which this merge is performed (effectively closing its branch). Certain merge-time state will force this merge type to silently behave like FSL_MERGE_TYPE_NORMAL: - If the being-merged-in content is marked as private. - If the being-merged-in content is not a leaf. */ FSL_MERGE_TYPE_INTEGRATE = -4, /** Indicates a cherrypick merge, pulling in only the changes made to a specific checkin without otherwise inheriting its lineage. */ FSL_MERGE_TYPE_CHERRYPICK = -1, /** Indicates a backout merge, a reverse cherrypick, backing out any changes which were added by the corresponding fsl_merge_opt::mergeRid. */ FSL_MERGE_TYPE_BACKOUT = -2 }; typedef enum fsl_merge_type_e fsl_merge_type_e; /** UNDER CONSTRUCTION. Options for use with fsl_ckout_merge(). */ struct fsl_merge_opt { /** The version of the repostitory to merge into the current checkout. This must be the `blob.rid` of a checkin artifact. */ fsl_id_t mergeRid; /** The version of the most recent common ancestor. Must normally be 0. The default is calculated automatically based on this->mergeRid and this->mergeType. This corresponds to fossil(1)'s `--baseline` merge flag. fsl_ckout_merge() will fail if this is >0 and it does not refer to a checkin version or if this->mergeType is FSL_MERGE_TYPE_CHERRYPICK. */ fsl_id_t baselineRid; /** Specifies the merge type to perform. Certain merge-internal logic may override this. Specifically, integrate-merges may be treated as regular merges, as documented for FSL_MERGE_TYPE_INTEGRATE. */ fsl_merge_type_e mergeType; /** Gets called once per merge-updated file, passed a fsl_ckup_state instance with information about the merged file and related metadata. May be NULL, in which case the merge process will do as much work as it can, even if that means doing certain questionable things (such as skipping updates of binary files or symlinks because it refuses to merge them). As a rule of thumb, if fossil(1) performs a given "questional" merge features without generating an error, fsl_ckout_merge() does as well. This callback gives clients a chance to decide that certain states _Simply Will Not Stand_ and cancel the merge by returning non-0 (preferably after calling fsl_cx_err_set() on the passed-in fsl_merge_state::f object). The callback is called after any on-disk changes are made to the file, e.g. merging, file permissions, renaming, etc. However, when this->dryRun is true, filesystem-level changes are skipped. */ fsl_merge_f callback; /** Client-defined state for use with this->callback. */ void * callbackState; /** A hint to fsl_ckout_merge() about whether it needs to scan the checkout for changes. Set this to false ONLY if the calling code calls fsl_ckout_changes_scan() (or equivalent, e.g. fsl_vfile_changes_scan()) immediately before calling fsl_ckout_merge(), as that function requires a non-stale changes scan in order to function properly. */ bool scanForChanges; /** If true, the extraction process will "go through the motions" but will not write any files to disk. It may still perform I/O such as stat()'ing to see, e.g., if it would have needed to overwrite a file. When in dry-run, this->callback is still called as if dry-run mode were not in effect. Thus the on-disk state may not actually reflect what the callback sees when dry-run mode is active. */ bool dryRun; /** This flag is not part of the public API and will be removed once the merge operation's development has settled down. */ unsigned short debug; /** TODO: - How to handle fossil's --binary GLOBPATTERN flag. Plain string or a glob list object or a stateful predicate function or... ? We currently rely entirely on the global `binary-glob` setting. */ }; /** Initialized-with-defaults fsl_merge_opt structure, intended for const-copy initialization. */ #define fsl_merge_opt_empty_m \ {-1/*mergeRid*/,0/*baselineRid*/, \ FSL_MERGE_TYPE_NORMAL/*mergeType*/, \ NULL/*callback*/, NULL/*callbackState*/, \ true/*scanForChanges*/, \ false/*dryRun*/,0/*debug*/} /** Initialized-with-defaults fsl_merge_opt structure, intended for non-const copy initialization. */ extern const fsl_merge_opt fsl_merge_opt_empty; /** UNDER CONSTRUCTION and not yet well-tested. Performs a "merge" operation on the current checkout, merging in version opt->mergeRid. If that version has already been merged, this call has no SCM-related side effects. Returns 0 on success, any number of non-0 codes on error, including, _but not limited to_: - FSL_RC_NOT_A_CKOUT if f has no opened checkout. - FSL_RC_OOM on allocation error. - FSL_RC_TYPE if opt->mergeRid or opt->baselineRid do to refer to a checkin. - FSL_RC_PHANTOM if a file participating in the merge is a phantom. - FSL_RC_RANGE if the to-be-merged-in RID is the same as the current checkout RID or the same as the pivot/baseline of the merge. - FSL_RC_NOT_FOUND if no common ancestor can be found for use as a basis for the merge. - FSL_RC_MISUSE if the current checkout is empty (has an RID of 0). - Any number of DB- or I/O-related codes, as well as codes from underlying APIs such as fsl_vfile_changes_scan(). For all but the most trivial argument validation errors or allocation errors, f's error state will be updated with a description of the problem. TODOs and potential TODOs: - There are certain illegal combinations of merge state which may require adding new result codes for. e.g. a no-op merge. - Empty directories may be left behind when a merge removes all files in a directory. fsl_ckout_update() handles that case but fsl_ckout_merge() currently does not. */ FSL_EXPORT int fsl_ckout_merge(fsl_cx * const f, fsl_merge_opt const * const opt); #if defined(__cplusplus) } /*extern "C"*/ #endif #endif /* ORG_FOSSIL_SCM_FSL_CHECKOUT_H_INCLUDED */