/*
** Copyright (c) 2007 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Author contact information:
** drh@hwaci.com
** http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains code used to check-in versions of the project
** from the local repository.
*/
#include "config.h"
#include "checkin.h"
#include <assert.h>
/*
** Generate text describing all changes. Prepend zPrefix to each line
** of output.
**
** We assume that vfile_check_signature has been run.
**
** If missingIsFatal is true, then any files that are missing or which
** are not true files results in a fatal error.
*/
static void status_report(
Blob *report, /* Append the status report here */
const char *zPrefix, /* Prefix on each line of the report */
int missingIsFatal, /* MISSING and NOT_A_FILE are fatal errors */
int cwdRelative /* Report relative to the current working dir */
){
Stmt q;
int nPrefix = strlen(zPrefix);
int nErr = 0;
Blob rewrittenPathname;
db_prepare(&q,
"SELECT pathname, deleted, chnged, rid, coalesce(origname!=pathname,0)"
" FROM vfile "
" WHERE file_is_selected(id)"
" AND (chnged OR deleted OR rid=0 OR pathname!=origname) ORDER BY 1"
);
blob_zero(&rewrittenPathname);
while( db_step(&q)==SQLITE_ROW ){
const char *zPathname = db_column_text(&q,0);
const char *zDisplayName = zPathname;
int isDeleted = db_column_int(&q, 1);
int isChnged = db_column_int(&q,2);
int isNew = db_column_int(&q,3)==0;
int isRenamed = db_column_int(&q,4);
char *zFullName = mprintf("%s%s", g.zLocalRoot, zPathname);
if( cwdRelative ){
file_relative_name(zFullName, &rewrittenPathname);
zDisplayName = blob_str(&rewrittenPathname);
if( zDisplayName[0]=='.' && zDisplayName[1]=='/' ){
zDisplayName += 2; /* no unnecessary ./ prefix */
}
}
blob_append(report, zPrefix, nPrefix);
if( isDeleted ){
blob_appendf(report, "DELETED %s\n", zDisplayName);
}else if( !file_isfile(zFullName) ){
if( file_access(zFullName, 0)==0 ){
blob_appendf(report, "NOT_A_FILE %s\n", zDisplayName);
if( missingIsFatal ){
fossil_warning("not a file: %s", zDisplayName);
nErr++;
}
}else{
blob_appendf(report, "MISSING %s\n", zDisplayName);
if( missingIsFatal ){
fossil_warning("missing file: %s", zDisplayName);
nErr++;
}
}
}else if( isNew ){
blob_appendf(report, "ADDED %s\n", zDisplayName);
}else if( isDeleted ){
blob_appendf(report, "DELETED %s\n", zDisplayName);
}else if( isChnged==2 ){
blob_appendf(report, "UPDATED_BY_MERGE %s\n", zDisplayName);
}else if( isChnged==3 ){
blob_appendf(report, "ADDED_BY_MERGE %s\n", zDisplayName);
}else if( isChnged==1 ){
blob_appendf(report, "EDITED %s\n", zDisplayName);
}else if( isRenamed ){
blob_appendf(report, "RENAMED %s\n", zDisplayName);
}
free(zFullName);
}
blob_reset(&rewrittenPathname);
db_finalize(&q);
db_prepare(&q, "SELECT uuid FROM vmerge JOIN blob ON merge=rid"
" WHERE id=0");
while( db_step(&q)==SQLITE_ROW ){
blob_append(report, zPrefix, nPrefix);
blob_appendf(report, "MERGED_WITH %s\n", db_column_text(&q, 0));
}
db_finalize(&q);
if( nErr ){
fossil_fatal("aborting due to prior errors");
}
}
/*
** Use the "relative-paths" setting and the --abs-paths and
** --rel-paths command line options to determine whether the
** status report should be shown relative to the current
** working directory.
*/
static int determine_cwd_relative_option()
{
int relativePaths = db_get_boolean("relative-paths", 1);
int absPathOption = find_option("abs-paths", 0, 0)!=0;
int relPathOption = find_option("rel-paths", 0, 0)!=0;
if( absPathOption ){ relativePaths = 0; }
if( relPathOption ){ relativePaths = 1; }
return relativePaths;
}
/*
** COMMAND: changes
**
** Usage: %fossil changes
**
** Report on the edit status of all files in the current checkout.
** See also the "status" and "extra" commands.
**
** Pathnames are displayed according to the "relative-paths" setting,
** unless overridden by the --abs-paths or --rel-paths options.
**
** Options:
**
** --sha1sum Verify file status using SHA1 hashing rather
** than relying on file mtimes.
**
** --abs-paths Display absolute pathnames.
**
** --rel-paths Display pathnames relative to the current working
** directory.
*/
void changes_cmd(void){
Blob report;
int vid;
int useSha1sum = find_option("sha1sum", 0, 0)!=0;
int cwdRelative = 0;
db_must_be_within_tree();
cwdRelative = determine_cwd_relative_option();
blob_zero(&report);
vid = db_lget_int("checkout", 0);
vfile_check_signature(vid, 0, useSha1sum);
status_report(&report, "", 0, cwdRelative);
blob_write_to_file(&report, "-");
}
/*
** COMMAND: status
**
** Usage: %fossil status
**
** Report on the status of the current checkout.
**
** Pathnames are displayed according to the "relative-paths" setting,
** unless overridden by the --abs-paths or --rel-paths options.
**
** Options:
**
** --sha1sum Verify file status using SHA1 hashing rather
** than relying on file mtimes.
**
** --abs-paths Display absolute pathnames.
**
** --rel-paths Display pathnames relative to the current working
** directory.
*/
void status_cmd(void){
int vid;
db_must_be_within_tree();
/* 012345678901234 */
fossil_print("repository: %s\n", db_lget("repository",""));
fossil_print("local-root: %s\n", g.zLocalRoot);
fossil_print("server-code: %s\n", db_get("server-code", ""));
vid = db_lget_int("checkout", 0);
if( vid ){
show_common_info(vid, "checkout:", 1, 1);
}
changes_cmd();
}
/*
** COMMAND: ls
**
** Usage: %fossil ls [-l]
**
** Show the names of all files in the current checkout. The -l provides
** extra information about each file.
*/
void ls_cmd(void){
int vid;
Stmt q;
int isBrief;
isBrief = find_option("l","l", 0)==0;
db_must_be_within_tree();
vid = db_lget_int("checkout", 0);
vfile_check_signature(vid, 0, 0);
db_prepare(&q,
"SELECT pathname, deleted, rid, chnged, coalesce(origname!=pathname,0)"
" FROM vfile"
" ORDER BY 1"
);
while( db_step(&q)==SQLITE_ROW ){
const char *zPathname = db_column_text(&q,0);
int isDeleted = db_column_int(&q, 1);
int isNew = db_column_int(&q,2)==0;
int chnged = db_column_int(&q,3);
int renamed = db_column_int(&q,4);
char *zFullName = mprintf("%s%s", g.zLocalRoot, zPathname);
if( isBrief ){
fossil_print("%s\n", zPathname);
}else if( isNew ){
fossil_print("ADDED %s\n", zPathname);
}else if( isDeleted ){
fossil_print("DELETED %s\n", zPathname);
}else if( !file_isfile(zFullName) ){
if( file_access(zFullName, 0)==0 ){
fossil_print("NOT_A_FILE %s\n", zPathname);
}else{
fossil_print("MISSING %s\n", zPathname);
}
}else if( chnged ){
fossil_print("EDITED %s\n", zPathname);
}else if( renamed ){
fossil_print("RENAMED %s\n", zPathname);
}else{
fossil_print("UNCHANGED %s\n", zPathname);
}
free(zFullName);
}
db_finalize(&q);
}
/*
** COMMAND: extras
** Usage: %fossil extras ?--dotfiles? ?--ignore GLOBPATTERN?
**
** Print a list of all files in the source tree that are not part of
** the current checkout. See also the "clean" command.
**
** Files and subdirectories whose names begin with "." are normally
** ignored but can be included by adding the --dotfiles option.
**
** The GLOBPATTERN is a comma-separated list of GLOB expressions for
** files that are ignored. The GLOBPATTERN specified by the "ignore-glob"
** is used if the --ignore option is omitted.
**
** Pathnames are displayed according to the "relative-paths" setting,
** unless overridden by the --abs-paths or --rel-paths options.
**
** Options:
**
** --dotfiles Include files with names beginning with "."
**
** --ignore GLOBPATTERN
** Override the "ignore-glob" setting.
**
** --abs-paths Display absolute pathnames.
**
** --rel-paths Display pathnames relative to the current working
** directory.
*/
void extra_cmd(void){
Blob path;
Blob repo;
Stmt q;
int n;
const char *zIgnoreFlag = find_option("ignore",0,1);
int allFlag = find_option("dotfiles",0,0)!=0;
int cwdRelative = 0;
int outputManifest;
Glob *pIgnore;
Blob rewrittenPathname;
const char *zPathname, *zDisplayName;
db_must_be_within_tree();
cwdRelative = determine_cwd_relative_option();
outputManifest = db_get_boolean("manifest",0);
db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)");
n = strlen(g.zLocalRoot);
blob_init(&path, g.zLocalRoot, n-1);
if( zIgnoreFlag==0 ){
zIgnoreFlag = db_get("ignore-glob", 0);
}
pIgnore = glob_create(zIgnoreFlag);
vfile_scan(&path, blob_size(&path), allFlag, pIgnore);
glob_free(pIgnore);
db_prepare(&q,
"SELECT x FROM sfile"
" WHERE x NOT IN (%s)"
" ORDER BY 1",
fossil_all_reserved_names()
);
if( file_tree_name(g.zRepositoryName, &repo, 0) ){
db_multi_exec("DELETE FROM sfile WHERE x=%B", &repo);
}
blob_zero(&rewrittenPathname);
while( db_step(&q)==SQLITE_ROW ){
zDisplayName = zPathname = db_column_text(&q, 0);
if( cwdRelative ) {
char *zFullName = mprintf("%s%s", g.zLocalRoot, zPathname);
file_relative_name(zFullName, &rewrittenPathname);
free(zFullName);
zDisplayName = blob_str(&rewrittenPathname);
if( zDisplayName[0]=='.' && zDisplayName[1]=='/' ){
zDisplayName += 2; /* no unnecessary ./ prefix */
}
}
fossil_print("%s\n", zDisplayName);
}
blob_reset(&rewrittenPathname);
db_finalize(&q);
}
/*
** COMMAND: clean
** Usage: %fossil clean ?--force? ?--dotfiles? ?--ignore GLOBPATTERN?
**
** Delete all "extra" files in the source tree. "Extra" files are
** files that are not officially part of the checkout. See also
** the "extra" command. This operation cannot be undone.
**
** You will be prompted before removing each file. If you are
** sure you wish to remove all "extra" files you can specify the
** optional --force flag and no prompts will be issued.
**
** Files and subdirectories whose names begin with "." are
** normally ignored. They are included if the "--dotfiles" option
** is used.
**
** The GLOBPATTERN is a comma-separated list of GLOB expressions for
** files that are ignored. The GLOBPATTERN specified by the "ignore-glob"
** is used if the --ignore option is omitted.
*/
void clean_cmd(void){
int allFlag;
int dotfilesFlag;
const char *zIgnoreFlag;
Blob path, repo;
Stmt q;
int n;
Glob *pIgnore;
allFlag = find_option("force","f",0)!=0;
dotfilesFlag = find_option("dotfiles",0,0)!=0;
zIgnoreFlag = find_option("ignore",0,1);
db_must_be_within_tree();
if( zIgnoreFlag==0 ){
zIgnoreFlag = db_get("ignore-glob", 0);
}
db_multi_exec("CREATE TEMP TABLE sfile(x TEXT PRIMARY KEY)");
n = strlen(g.zLocalRoot);
blob_init(&path, g.zLocalRoot, n-1);
pIgnore = glob_create(zIgnoreFlag);
vfile_scan(&path, blob_size(&path), dotfilesFlag, pIgnore);
glob_free(pIgnore);
db_prepare(&q,
"SELECT %Q || x FROM sfile"
" WHERE x NOT IN (%s)"
" ORDER BY 1",
g.zLocalRoot, fossil_all_reserved_names()
);
if( file_tree_name(g.zRepositoryName, &repo, 0) ){
db_multi_exec("DELETE FROM sfile WHERE x=%B", &repo);
}
while( db_step(&q)==SQLITE_ROW ){
if( allFlag ){
file_delete(db_column_text(&q, 0));
}else{
Blob ans;
char *prompt = mprintf("remove unmanaged file \"%s\" (y/N)? ",
db_column_text(&q, 0));
blob_zero(&ans);
prompt_user(prompt, &ans);
if( blob_str(&ans)[0]=='y' ){
file_delete(db_column_text(&q, 0));
}
}
}
db_finalize(&q);
}
/*
** Prepare a commit comment. Let the user modify it using the
** editor specified in the global_config table or either
** the VISUAL or EDITOR environment variable.
**
** Store the final commit comment in pComment. pComment is assumed
** to be uninitialized - any prior content is overwritten.
**
** zInit is the text of the most recent failed attempt to check in
** this same change. Use zInit to reinitialize the check-in comment
** so that the user does not have to retype.
**
** zBranch is the name of a new branch that this check-in is forced into.
** zBranch might be NULL or an empty string if no forcing occurs.
**
** parent_rid is the recordid of the parent check-in.
*/
static void prepare_commit_comment(
Blob *pComment,
char *zInit,
const char *zBranch,
int parent_rid
){
const char *zEditor;
char *zCmd;
char *zFile;
Blob text, line;
char *zComment;
int i;
blob_init(&text, zInit, -1);
blob_append(&text,
"\n"
"# Enter comments on this check-in. Lines beginning with # are ignored.\n"
"# The check-in comment follows wiki formatting rules.\n"
"#\n", -1
);
blob_appendf(&text, "# user: %s\n", g.zLogin);
if( zBranch && zBranch[0] ){
blob_appendf(&text, "# tags: %s\n#\n", zBranch);
}else{
char *zTags = info_tags_of_checkin(parent_rid, 1);
if( zTags ) blob_appendf(&text, "# tags: %z\n#\n", zTags);
}
if( g.markPrivate ){
blob_append(&text,
"# PRIVATE BRANCH: This check-in will be private and will not sync to\n"
"# repositories.\n"
"#\n", -1
);
}
status_report(&text, "# ", 1, 0);
zEditor = db_get("editor", 0);
if( zEditor==0 ){
zEditor = getenv("VISUAL");
}
if( zEditor==0 ){
zEditor = getenv("EDITOR");
}
if( zEditor==0 ){
blob_append(&text,
"#\n"
"# Since no default text editor is set using EDITOR or VISUAL\n"
"# environment variables or the \"fossil set editor\" command,\n"
"# and because no check-in comment was specified using the \"-m\"\n"
"# or \"-M\" command-line options, you will need to enter the\n"
"# check-in comment below. Type \".\" on a line by itself when\n"
"# you are done:\n", -1);
zFile = mprintf("-");
}else{
zFile = db_text(0, "SELECT '%qci-comment-' || hex(randomblob(6)) || '.txt'",
g.zLocalRoot);
}
#if defined(_WIN32)
blob_add_cr(&text);
#endif
blob_write_to_file(&text, zFile);
if( zEditor ){
zCmd = mprintf("%s \"%s\"", zEditor, zFile);
fossil_print("%s\n", zCmd);
if( fossil_system(zCmd) ){
fossil_panic("editor aborted");
}
blob_reset(&text);
blob_read_from_file(&text, zFile);
}else{
char zIn[300];
blob_reset(&text);
while( fgets(zIn, sizeof(zIn), stdin)!=0 ){
char *zUtf8 = fossil_mbcs_to_utf8(zIn);
if( zUtf8[0]=='.' && (zUtf8[1]==0 || zUtf8[1]=='\r' || zUtf8[1]=='\n') ){
fossil_mbcs_free(zUtf8);
break;
}
blob_append(&text, zIn, -1);
fossil_mbcs_free(zUtf8);
}
}
blob_remove_cr(&text);
file_delete(zFile);
free(zFile);
blob_zero(pComment);
while( blob_line(&text, &line) ){
int i, n;
char *z;
n = blob_size(&line);
z = blob_buffer(&line);
for(i=0; i<n && fossil_isspace(z[i]); i++){}
if( i<n && z[i]=='#' ) continue;
if( i<n || blob_size(pComment)>0 ){
blob_appendf(pComment, "%b", &line);
}
}
blob_reset(&text);
zComment = blob_str(pComment);
i = strlen(zComment);
while( i>0 && fossil_isspace(zComment[i-1]) ){ i--; }
blob_resize(pComment, i);
}
/*
** Populate the Global.aCommitFile[] based on the command line arguments
** to a [commit] command. Global.aCommitFile is an array of integers
** sized at (N+1), where N is the number of arguments passed to [commit].
** The contents are the [id] values from the vfile table corresponding
** to the filenames passed as arguments.
**
** The last element of aCommitFile[] is always 0 - indicating the end
** of the array.
**
** If there were no arguments passed to [commit], aCommitFile is not
** allocated and remains NULL. Other parts of the code interpret this
** to mean "all files".
*/
void select_commit_files(void){
if( g.argc>2 ){
int ii;
Blob b;
blob_zero(&b);
g.aCommitFile = fossil_malloc(sizeof(int)*(g.argc-1));
for(ii=2; ii<g.argc; ii++){
int iId;
file_tree_name(g.argv[ii], &b, 1);
iId = db_int(-1, "SELECT id FROM vfile WHERE pathname=%Q", blob_str(&b));
if( iId<0 ){
fossil_fatal("fossil knows nothing about: %s", g.argv[ii]);
}
g.aCommitFile[ii-2] = iId;
blob_reset(&b);
}
g.aCommitFile[ii-2] = 0;
}
}
/*
** Make sure the current check-in with timestamp zDate is younger than its
** ancestor identified rid and zUuid. Throw a fatal error if not.
*/
static void checkin_verify_younger(
int rid, /* The record ID of the ancestor */
const char *zUuid, /* The artifact ID of the ancestor */
const char *zDate /* Date & time of the current check-in */
){
#ifndef FOSSIL_ALLOW_OUT_OF_ORDER_DATES
int b;
b = db_exists(
"SELECT 1 FROM event"
" WHERE datetime(mtime)>=%Q"
" AND type='ci' AND objid=%d",
zDate, rid
);
if( b ){
fossil_fatal("ancestor check-in [%.10s] (%s) is not older (clock skew?)"
" Use -f to override.", zUuid, zDate);
}
#endif
}
/*
** zDate should be a valid date string. Convert this string into the
** format YYYY-MM-DDTHH:MM:SS. If the string is not a valid date,
** print a fatal error and quit.
*/
char *date_in_standard_format(const char *zInputDate){
char *zDate;
zDate = db_text(0, "SELECT strftime('%%Y-%%m-%%dT%%H:%%M:%%f',%Q)",
zInputDate);
if( zDate[0]==0 ){
fossil_fatal(
"unrecognized date format (%s): use \"YYYY-MM-DD HH:MM:SS.SSS\"",
zInputDate
);
}
return zDate;
}
/*
** Create a manifest.
*/
static void create_manifest(
Blob *pOut, /* Write the manifest here */
const char *zBaselineUuid, /* UUID of baseline, or zero */
Manifest *pBaseline, /* Make it a delta manifest if not zero */
Blob *pComment, /* Check-in comment text */
int vid, /* blob-id of the parent manifest */
int verifyDate, /* Verify that child is younger */
Blob *pCksum, /* Repository checksum. May be 0 */
const char *zDateOvrd, /* Date override. If 0 then use 'now' */
const char *zUserOvrd, /* User override. If 0 then use g.zLogin */
const char *zBranch, /* Branch name. May be 0 */
const char *zBgColor, /* Background color. May be 0 */
const char *zTag, /* Tag to apply to this check-in */
int *pnFBcard /* Number of generated B- and F-cards */
){
char *zDate; /* Date of the check-in */
char *zParentUuid; /* UUID of parent check-in */
Blob filename; /* A single filename */
int nBasename; /* Size of base filename */
Stmt q; /* Query of files changed */
Stmt q2; /* Query of merge parents */
Blob mcksum; /* Manifest checksum */
ManifestFile *pFile; /* File from the baseline */
int nFBcard = 0; /* Number of B-cards and F-cards */
assert( pBaseline==0 || pBaseline->zBaseline==0 );
assert( pBaseline==0 || zBaselineUuid!=0 );
blob_zero(pOut);
zParentUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid);
if( pBaseline ){
blob_appendf(pOut, "B %s\n", zBaselineUuid);
manifest_file_rewind(pBaseline);
pFile = manifest_file_next(pBaseline, 0);
nFBcard++;
}else{
pFile = 0;
}
blob_appendf(pOut, "C %F\n", blob_str(pComment));
zDate = date_in_standard_format(zDateOvrd ? zDateOvrd : "now");
blob_appendf(pOut, "D %s\n", zDate);
zDate[10] = ' ';
db_prepare(&q,
"SELECT pathname, uuid, origname, blob.rid, isexe,"
" file_is_selected(vfile.id)"
" FROM vfile JOIN blob ON vfile.mrid=blob.rid"
" WHERE (NOT deleted OR NOT file_is_selected(vfile.id))"
" AND vfile.vid=%d"
" ORDER BY 1", vid);
blob_zero(&filename);
blob_appendf(&filename, "%s", g.zLocalRoot);
nBasename = blob_size(&filename);
while( db_step(&q)==SQLITE_ROW ){
const char *zName = db_column_text(&q, 0);
const char *zUuid = db_column_text(&q, 1);
const char *zOrig = db_column_text(&q, 2);
int frid = db_column_int(&q, 3);
int isexe = db_column_int(&q, 4);
int isSelected = db_column_int(&q, 5);
const char *zPerm;
int cmp;
#if !defined(_WIN32)
/* For unix, extract the "executable" permission bit directly from
** the filesystem. On windows, the "executable" bit is retained
** unchanged from the original.
*/
blob_resize(&filename, nBasename);
blob_append(&filename, zName, -1);
isexe = file_isexe(blob_str(&filename));
#endif
if( isexe ){
zPerm = " x";
}else{
zPerm = "";
}
if( !g.markPrivate ) content_make_public(frid);
while( pFile && fossil_strcmp(pFile->zName,zName)<0 ){
blob_appendf(pOut, "F %F\n", pFile->zName);
pFile = manifest_file_next(pBaseline, 0);
nFBcard++;
}
cmp = 1;
if( pFile==0
|| (cmp = fossil_strcmp(pFile->zName,zName))!=0
|| fossil_strcmp(pFile->zUuid, zUuid)!=0
){
if( zOrig && !isSelected ){ zName = zOrig; zOrig = 0; }
if( zOrig==0 || fossil_strcmp(zOrig,zName)==0 ){
blob_appendf(pOut, "F %F %s%s\n", zName, zUuid, zPerm);
}else{
if( zPerm[0]==0 ){ zPerm = " w"; }
blob_appendf(pOut, "F %F %s%s %F\n", zName, zUuid, zPerm, zOrig);
}
nFBcard++;
}
if( cmp==0 ) pFile = manifest_file_next(pBaseline,0);
}
blob_reset(&filename);
db_finalize(&q);
while( pFile ){
blob_appendf(pOut, "F %F\n", pFile->zName);
pFile = manifest_file_next(pBaseline, 0);
nFBcard++;
}
blob_appendf(pOut, "P %s", zParentUuid);
if( verifyDate ) checkin_verify_younger(vid, zParentUuid, zDate);
free(zParentUuid);
db_prepare(&q2, "SELECT merge FROM vmerge WHERE id=:id");
db_bind_int(&q2, ":id", 0);
while( db_step(&q2)==SQLITE_ROW ){
char *zMergeUuid;
int mid = db_column_int(&q2, 0);
if( !g.markPrivate && content_is_private(mid) ) continue;
zMergeUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", mid);
if( zMergeUuid ){
blob_appendf(pOut, " %s", zMergeUuid);
if( verifyDate ) checkin_verify_younger(mid, zMergeUuid, zDate);
free(zMergeUuid);
}
}
db_finalize(&q2);
free(zDate);
blob_appendf(pOut, "\n");
if( pCksum ) blob_appendf(pOut, "R %b\n", pCksum);
if( zBranch && zBranch[0] ){
/* Set tags for the new branch */
if( zBgColor && zBgColor[0] ){
blob_appendf(pOut, "T *bgcolor * %F\n", zBgColor);
}
blob_appendf(pOut, "T *branch * %F\n", zBranch);
blob_appendf(pOut, "T *sym-%F *\n", zBranch);
}
if( g.markPrivate ){
/* If this manifest is private, mark it as such */
blob_appendf(pOut, "T +private *\n");
}
if( zTag && zTag[0] ){
/* Add a symbolic tag to this check-in */
blob_appendf(pOut, "T +sym-%F *\n", zTag);
}
if( zBranch && zBranch[0] ){
/* For a new branch, cancel all prior propagating tags */
Stmt q;
db_prepare(&q,
"SELECT tagname FROM tagxref, tag"
" WHERE tagxref.rid=%d AND tagxref.tagid=tag.tagid"
" AND tagtype==2 AND tagname GLOB 'sym-*'"
" AND tagname!='sym-'||%Q"
" ORDER BY tagname",
vid, zBranch);
while( db_step(&q)==SQLITE_ROW ){
const char *zTag = db_column_text(&q, 0);
blob_appendf(pOut, "T -%F *\n", zTag);
}
db_finalize(&q);
}
blob_appendf(pOut, "U %F\n", zUserOvrd ? zUserOvrd : g.zLogin);
md5sum_blob(pOut, &mcksum);
blob_appendf(pOut, "Z %b\n", &mcksum);
if( pnFBcard ) *pnFBcard = nFBcard;
}
/*
** Issue a warning and give the user an opportunity to abandon out
** if a \r\n line ending is seen in a text file.
*/
static void cr_warning(const Blob *p, const char *zFilename){
int nCrNl = 0; /* Number of \r\n line endings seen */
const unsigned char *z; /* File text */
int n; /* Size of the file in bytes */
int lastNl = 0; /* Characters since last \n */
int i; /* Loop counter */
char *zMsg; /* Warning message */
Blob fname; /* Relative pathname of the file */
Blob ans; /* Answer to continue prompt */
static int allOk = 0; /* Set to true to disable this routine */
if( allOk ) return;
z = (unsigned char*)blob_buffer(p);
n = blob_size(p);
for(i=0; i<n-1; i++){
unsigned char c = z[i];
if( c==0 ) return; /* It's binary */
if( c=='\n' ){
if( i>0 && z[i-1]=='\r' ){
nCrNl++;
if( i>10000 ) break;
}
lastNl = 0;
}else{
lastNl++;
if( lastNl>1000 ) return; /* Binary if any line longer than 1000 */
}
}
if( nCrNl ){
char c;
file_relative_name(zFilename, &fname);
blob_zero(&ans);
zMsg = mprintf(
"%s contains CR/NL line endings; commit anyhow (yes/no/all)?",
blob_str(&fname));
prompt_user(zMsg, &ans);
fossil_free(zMsg);
c = blob_str(&ans)[0];
if( c=='a' ){
allOk = 1;
}else if( c!='y' ){
fossil_fatal("Abandoning commit due to CR+NL line endings in %s",
blob_str(&fname));
}
blob_reset(&ans);
blob_reset(&fname);
}
}
/*
** COMMAND: ci
** COMMAND: commit
**
** Usage: %fossil commit ?OPTIONS? ?FILE...?
**
** Create a new version containing all of the changes in the current
** checkout. You will be prompted to enter a check-in comment unless
** the comment has been specified on the command-line using "-m" or a
** file containing the comment using -M. The editor defined in the
** "editor" fossil option (see %fossil help set) will be used, or from
** the "VISUAL" or "EDITOR" environment variables (in that order) if
** no editor is set.
**
** All files that have changed will be committed unless some subset of
** files is specified on the command line.
**
** The --branch option followed by a branch name causes the new check-in
** to be placed in the named branch. The --bgcolor option can be followed
** by a color name (ex: '#ffc0c0') to specify the background color of
** entries in the new branch when shown in the web timeline interface.
**
** A check-in is not permitted to fork unless the --force or -f
** option appears. A check-in is not allowed against a closed check-in.
**
** The --private option creates a private check-in that is never synced.
** Children of private check-ins are automatically private.
**
** the --tag option applies the symbolic tag name to the check-in.
**
** Options:
**
** --comment|-m COMMENT-TEXT
** --message-file|-M COMMENT-FILE
** --branch NEW-BRANCH-NAME
** --bgcolor COLOR
** --nosign
** --force|-f
** --private
** --baseline
** --delta
** --tag TAG-NAME
**
*/
void commit_cmd(void){
int hasChanges; /* True if unsaved changes exist */
int vid; /* blob-id of parent version */
int nrid; /* blob-id of a modified file */
int nvid; /* Blob-id of the new check-in */
Blob comment; /* Check-in comment */
const char *zComment; /* Check-in comment */
Stmt q; /* Query to find files that have been modified */
char *zUuid; /* UUID of the new check-in */
int noSign = 0; /* True to omit signing the manifest using GPG */
int isAMerge = 0; /* True if checking in a merge */
int forceFlag = 0; /* Force a fork */
int forceDelta = 0; /* Force a delta-manifest */
int forceBaseline = 0; /* Force a baseline-manifest */
char *zManifestFile; /* Name of the manifest file */
int useCksum; /* True if checksums should be computed and verified */
int outputManifest; /* True to output "manifest" and "manifest.uuid" */
int testRun; /* True for a test run. Debugging only */
const char *zBranch; /* Create a new branch with this name */
const char *zBgColor; /* Set background color when branching */
const char *zDateOvrd; /* Override date string */
const char *zUserOvrd; /* Override user name */
const char *zComFile; /* Read commit message from this file */
const char *zTag; /* Symbolic tag to apply to this check-in */
Blob manifest; /* Manifest in baseline form */
Blob muuid; /* Manifest uuid */
Blob cksum1, cksum2; /* Before and after commit checksums */
Blob cksum1b; /* Checksum recorded in the manifest */
int szD; /* Size of the delta manifest */
int szB; /* Size of the baseline manifest */
url_proxy_options();
noSign = find_option("nosign",0,0)!=0;
forceDelta = find_option("delta",0,0)!=0;
forceBaseline = find_option("baseline",0,0)!=0;
if( forceDelta && forceBaseline ){
fossil_fatal("cannot use --delta and --baseline together");
}
testRun = find_option("test",0,0)!=0;
zComment = find_option("comment","m",1);
forceFlag = find_option("force", "f", 0)!=0;
zBranch = find_option("branch","b",1);
zBgColor = find_option("bgcolor",0,1);
zTag = find_option("tag",0,1);
zComFile = find_option("message-file", "M", 1);
if( find_option("private",0,0) ){
g.markPrivate = 1;
if( zBranch==0 ) zBranch = "private";
if( zBgColor==0 ) zBgColor = "#fec084"; /* Orange */
}
zDateOvrd = find_option("date-override",0,1);
zUserOvrd = find_option("user-override",0,1);
db_must_be_within_tree();
noSign = db_get_boolean("omitsign", 0)|noSign;
if( db_get_boolean("clearsign", 0)==0 ){ noSign = 1; }
useCksum = db_get_boolean("repo-cksum", 1);
outputManifest = db_get_boolean("manifest", 0);
verify_all_options();
/* So that older versions of Fossil (that do not understand delta-
** manifest) can continue to use this repository, do not create a new
** delta-manifest unless this repository already contains one or more
** delta-manifets, or unless the delta-manifest is explicitly requested
** by the --delta option.
*/
if( !forceDelta && !db_get_boolean("seen-delta-manifest",0) ){
forceBaseline = 1;
}
/* Get the ID of the parent manifest artifact */
vid = db_lget_int("checkout", 0);
if( content_is_private(vid) ){
g.markPrivate = 1;
}
/*
** Autosync if autosync is enabled and this is not a private check-in.
*/
if( !g.markPrivate ){
if( autosync(AUTOSYNC_PULL) ){
Blob ans;
blob_zero(&ans);
prompt_user("continue in spite of sync failure (y/N)? ", &ans);
if( blob_str(&ans)[0]!='y' ){
fossil_exit(1);
}
}
}
/* Require confirmation to continue with the check-in if there is
** clock skew
*/
if( g.clockSkewSeen ){
Blob ans;
blob_zero(&ans);
prompt_user("continue in spite of time skew (y/N)? ", &ans);
if( blob_str(&ans)[0]!='y' ){
fossil_exit(1);
}
}
/* There are two ways this command may be executed. If there are
** no arguments following the word "commit", then all modified files
** in the checked out directory are committed. If one or more arguments
** follows "commit", then only those files are committed.
**
** After the following function call has returned, the Global.aCommitFile[]
** array is allocated to contain the "id" field from the vfile table
** for each file to be committed. Or, if aCommitFile is NULL, all files
** should be committed.
*/
select_commit_files();
isAMerge = db_exists("SELECT 1 FROM vmerge");
if( g.aCommitFile && isAMerge ){
fossil_fatal("cannot do a partial commit of a merge");
}
user_select();
/*
** Check that the user exists.
*/
if( !db_exists("SELECT 1 FROM user WHERE login=%Q", g.zLogin) ){
fossil_fatal("no such user: %s", g.zLogin);
}
hasChanges = unsaved_changes();
db_begin_transaction();
db_record_repository_filename(0);
if( hasChanges==0 && !isAMerge && !forceFlag ){
fossil_fatal("nothing has changed");
}
/* If one or more files that were named on the command line have not
** been modified, bail out now.
*/
if( g.aCommitFile ){
Blob unmodified;
memset(&unmodified, 0, sizeof(Blob));
blob_init(&unmodified, 0, 0);
db_blob(&unmodified,
"SELECT pathname FROM vfile"
" WHERE chnged = 0 AND origname IS NULL AND file_is_selected(id)"
);
if( strlen(blob_str(&unmodified)) ){
fossil_fatal("file %s has not changed", blob_str(&unmodified));
}
}
/*
** Do not allow a commit that will cause a fork unless the --force flag
** is used or unless this is a private check-in.
*/
if( zBranch==0 && forceFlag==0 && g.markPrivate==0 && !is_a_leaf(vid) ){
fossil_fatal("would fork. \"update\" first or use -f or --force.");
}
/*
** Do not allow a commit against a closed leaf
*/
if( db_exists("SELECT 1 FROM tagxref"
" WHERE tagid=%d AND rid=%d AND tagtype>0",
TAG_CLOSED, vid) ){
fossil_fatal("cannot commit against a closed leaf");
}
if( useCksum ) vfile_aggregate_checksum_disk(vid, &cksum1);
if( zComment ){
blob_zero(&comment);
blob_append(&comment, zComment, -1);
}else if( zComFile ){
blob_zero(&comment);
blob_read_from_file(&comment, zComFile);
}else{
char *zInit = db_text(0, "SELECT value FROM vvar WHERE name='ci-comment'");
prepare_commit_comment(&comment, zInit, zBranch, vid);
free(zInit);
}
if( blob_size(&comment)==0 ){
Blob ans;
blob_zero(&ans);
prompt_user("empty check-in comment. continue (y/N)? ", &ans);
if( blob_str(&ans)[0]!='y' ){
fossil_exit(1);
}
}else{
db_multi_exec("REPLACE INTO vvar VALUES('ci-comment',%B)", &comment);
db_end_transaction(0);
db_begin_transaction();
}
/* Step 1: Insert records for all modified files into the blob
** table. If there were arguments passed to this command, only
** the identified fils are inserted (if they have been modified).
*/
db_prepare(&q,
"SELECT id, %Q || pathname, mrid, %s FROM vfile "
"WHERE chnged==1 AND NOT deleted AND file_is_selected(id)",
g.zLocalRoot, glob_expr("pathname", db_get("crnl-glob",""))
);
while( db_step(&q)==SQLITE_ROW ){
int id, rid;
const char *zFullname;
Blob content;
int crnlOk;
id = db_column_int(&q, 0);
zFullname = db_column_text(&q, 1);
rid = db_column_int(&q, 2);
crnlOk = db_column_int(&q, 3);
blob_zero(&content);
blob_read_from_file(&content, zFullname);
if( !crnlOk ) cr_warning(&content, zFullname);
nrid = content_put(&content);
blob_reset(&content);
if( rid>0 ){
content_deltify(rid, nrid, 0);
}
db_multi_exec("UPDATE vfile SET mrid=%d, rid=%d WHERE id=%d", nrid,nrid,id);
db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid);
}
db_finalize(&q);
/* Create the new manifest */
if( blob_size(&comment)==0 ){
blob_append(&comment, "(no comment)", -1);
}
if( forceDelta ){
blob_zero(&manifest);
}else{
create_manifest(&manifest, 0, 0, &comment, vid,
!forceFlag, useCksum ? &cksum1 : 0,
zDateOvrd, zUserOvrd, zBranch, zBgColor, zTag, &szB);
}
/* See if a delta-manifest would be more appropriate */
if( !forceBaseline ){
const char *zBaselineUuid;
Manifest *pParent;
Manifest *pBaseline;
pParent = manifest_get(vid, CFTYPE_MANIFEST);
if( pParent && pParent->zBaseline ){
zBaselineUuid = pParent->zBaseline;
pBaseline = manifest_get_by_name(zBaselineUuid, 0);
}else{
zBaselineUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid);
pBaseline = pParent;
}
if( pBaseline ){
Blob delta;
create_manifest(&delta, zBaselineUuid, pBaseline, &comment, vid,
!forceFlag, useCksum ? &cksum1 : 0,
zDateOvrd, zUserOvrd, zBranch, zBgColor, zTag, &szD);
/*
** At this point, two manifests have been constructed, either of
** which would work for this checkin. The first manifest (held
** in the "manifest" variable) is a baseline manifest and the second
** (held in variable named "delta") is a delta manifest. The
** question now is: which manifest should we use?
**
** Let B be the number of F-cards in the baseline manifest and
** let D be the number of F-cards in the delta manifest, plus one for
** the B-card. (B is held in the szB variable and D is held in the
** szD variable.) Assume that all delta manifests adds X new F-cards.
** Then to minimize the total number of F- and B-cards in the repository,
** we should use the delta manifest if and only if:
**
** D*D < B*X - X*X
**
** X is an unknown here, but for most repositories, we will not be
** far wrong if we assume X=3.
*/
if( forceDelta || (szD*szD)<(szB*3-9) ){
blob_reset(&manifest);
manifest = delta;
}else{
blob_reset(&delta);
}
}else if( forceDelta ){
fossil_panic("unable to find a baseline-manifest for the delta");
}
}
if( !noSign && !g.markPrivate && clearsign(&manifest, &manifest) ){
Blob ans;
blob_zero(&ans);
prompt_user("unable to sign manifest. continue (y/N)? ", &ans);
if( blob_str(&ans)[0]!='y' ){
fossil_exit(1);
}
}
/* If the --test option is specified, output the manifest file
** and rollback the transaction.
*/
if( testRun ){
blob_write_to_file(&manifest, "");
}
if( outputManifest ){
zManifestFile = mprintf("%smanifest", g.zLocalRoot);
blob_write_to_file(&manifest, zManifestFile);
blob_reset(&manifest);
blob_read_from_file(&manifest, zManifestFile);
free(zManifestFile);
}
nvid = content_put(&manifest);
if( nvid==0 ){
fossil_panic("trouble committing manifest: %s", g.zErrMsg);
}
db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nvid);
manifest_crosslink(nvid, &manifest);
assert( blob_is_reset(&manifest) );
content_deltify(vid, nvid, 0);
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", nvid);
fossil_print("New_Version: %s\n", zUuid);
if( outputManifest ){
zManifestFile = mprintf("%smanifest.uuid", g.zLocalRoot);
blob_zero(&muuid);
blob_appendf(&muuid, "%s\n", zUuid);
blob_write_to_file(&muuid, zManifestFile);
free(zManifestFile);
blob_reset(&muuid);
}
/* Update the vfile and vmerge tables */
db_multi_exec(
"DELETE FROM vfile WHERE (vid!=%d OR deleted) AND file_is_selected(id);"
"DELETE FROM vmerge WHERE file_is_selected(id) OR id=0;"
"UPDATE vfile SET vid=%d;"
"UPDATE vfile SET rid=mrid, chnged=0, deleted=0, origname=NULL"
" WHERE file_is_selected(id);"
, vid, nvid
);
db_lset_int("checkout", nvid);
if( useCksum ){
/* Verify that the repository checksum matches the expected checksum
** calculated before the checkin started (and stored as the R record
** of the manifest file).
*/
vfile_aggregate_checksum_repository(nvid, &cksum2);
if( blob_compare(&cksum1, &cksum2) ){
vfile_compare_repository_to_disk(nvid);
fossil_fatal("working checkout does not match what would have ended "
"up in the repository: %b versus %b",
&cksum1, &cksum2);
}
/* Verify that the manifest checksum matches the expected checksum */
vfile_aggregate_checksum_manifest(nvid, &cksum2, &cksum1b);
if( blob_compare(&cksum1, &cksum1b) ){
fossil_fatal("manifest checksum self-test failed: "
"%b versus %b", &cksum1, &cksum1b);
}
if( blob_compare(&cksum1, &cksum2) ){
fossil_fatal(
"working checkout does not match manifest after commit: "
"%b versus %b", &cksum1, &cksum2);
}
/* Verify that the commit did not modify any disk images. */
vfile_aggregate_checksum_disk(nvid, &cksum2);
if( blob_compare(&cksum1, &cksum2) ){
fossil_fatal("working checkout before and after commit does not match");
}
}
/* Clear the undo/redo stack */
undo_reset();
/* Commit */
db_multi_exec("DELETE FROM vvar WHERE name='ci-comment'");
if( testRun ){
db_end_transaction(1);
exit(1);
}
db_end_transaction(0);
if( !g.markPrivate ){
autosync(AUTOSYNC_PUSH);
}
if( count_nonbranch_children(vid)>1 ){
fossil_print("**** warning: a fork has occurred *****\n");
}
}