diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/update.c | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'src/update.c')
| -rw-r--r-- | src/update.c | 1060 |
1 files changed, 1060 insertions, 0 deletions
diff --git a/src/update.c b/src/update.c new file mode 100644 index 0000000..b606049 --- /dev/null +++ b/src/update.c @@ -0,0 +1,1060 @@ +/* +** 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 merge the changes in the current +** check-out into a different version and switch to that version. +*/ +#include "config.h" +#include "update.h" +#include <assert.h> + +/* +** Return true if artifact rid is a version +*/ +int is_a_version(int rid){ + return db_exists("SELECT 1 FROM event WHERE objid=%d AND type='ci'", rid); +} + +/* This variable is set if we are doing an internal update. It is clear +** when running the "update" command. +*/ +static int internalUpdate = 0; +static int internalConflictCnt = 0; + +/* +** Do an update to version vid. +** +** Start an undo session but do not terminate it. Do not autosync. +*/ +int update_to(int vid){ + int savedArgc; + char **savedArgv; + char *newArgv[3]; + newArgv[0] = g.argv[0]; + newArgv[1] = "update"; + newArgv[2] = 0; + savedArgv = g.argv; + savedArgc = g.argc; + g.argc = 2; + g.argv = newArgv; + internalUpdate = vid; + internalConflictCnt = 0; + update_cmd(); + g.argc = savedArgc; + g.argv = savedArgv; + return internalConflictCnt; +} + +/* +** COMMAND: update +** +** Usage: %fossil update ?OPTIONS? ?VERSION? ?FILES...? +** +** Change the version of the current check-out to VERSION. Any +** uncommitted changes are retained and applied to the new check-out. +** +** The VERSION argument can be a specific version or tag or branch +** name. If the VERSION argument is omitted, then the leaf of the +** subtree that begins at the current version is used, if there is +** only a single leaf. VERSION can also be "current" to select the +** leaf of the current version or "latest" to select the most recent +** check-in. +** +** If one or more FILES are listed after the VERSION then only the +** named files are candidates to be updated, and any updates to them +** will be treated as edits to the current version. Using a directory +** name for one of the FILES arguments is the same as using every +** subdirectory and file beneath that directory. +** +** If FILES is omitted, all files in the current check-out are subject +** to being updated and the version of the current check-out is changed +** to VERSION. Any uncommitted changes are retained and applied to the +** new check-out. +** +** The -n or --dry-run option causes this command to do a "dry run". +** It prints out what would have happened but does not actually make +** any changes to the current check-out or the repository. +** +** The -v or --verbose option prints status information about +** unchanged files in addition to those files that actually do change. +** +** Options: +** --case-sensitive BOOL Override case-sensitive setting +** --debug Print debug information on stdout +** -n|--dry-run If given, display instead of run actions +** --force-missing Force update if missing content after sync +** -K|--keep-merge-files On merge conflict, retain the temporary files +** used for merging, named *-baseline, *-original, +** and *-merge. +** --latest Acceptable in place of VERSION, update to +** latest version +** --nosync Do not auto-sync prior to update +** --proxy PROXY Use PROXY as http proxy during sync operation +** --setmtime Set timestamps of all files to match their +** SCM-side times (the timestamp of the last +** check-in which modified them). +** -v|--verbose Print status information about all files +** -W|--width WIDTH Width of lines (default is to auto-detect). +** Must be more than 20 or 0 (= no limit, +** resulting in a single line per entry). +** +** See also: [[revert]] +*/ +void update_cmd(void){ + int vid; /* Current version */ + int tid=0; /* Target version - version we are changing to */ + Stmt q; + int latestFlag; /* --latest. Pick the latest version if true */ + int dryRunFlag; /* -n or --dry-run. Do a dry run */ + int verboseFlag; /* -v or --verbose. Output extra information */ + int forceMissingFlag; /* --force-missing. Continue if missing content */ + int debugFlag; /* --debug option */ + int setmtimeFlag; /* --setmtime. Set mtimes on files */ + int keepMergeFlag; /* True if --keep-merge-files is present */ + int nChng; /* Number of file renames */ + int *aChng; /* Array of file renames */ + int i; /* Loop counter */ + int nConflict = 0; /* Number of merge conflicts */ + int nOverwrite = 0; /* Number of unmanaged files overwritten */ + int nUpdate = 0; /* Number of changes of any kind */ + int bNosync = 0; /* --nosync. Omit the auto-sync */ + int width; /* Width of printed comment lines */ + Stmt mtimeXfer; /* Statement to transfer mtimes */ + const char *zWidth; /* Width option string value */ + const char *zCurBrName; /* Current branch name */ + const char *zNewBrName; /* New branch name */ + const char *zBrChgMsg = ""; /* Message to display if branch changes */ + + if( !internalUpdate ){ + undo_capture_command_line(); + url_proxy_options(); + } + zWidth = find_option("width","W",1); + if( zWidth ){ + width = atoi(zWidth); + if( (width!=0) && (width<=20) ){ + fossil_fatal("-W|--width value must be >20 or 0"); + } + }else{ + width = -1; + } + latestFlag = find_option("latest",0, 0)!=0; + dryRunFlag = find_option("dry-run","n",0)!=0; + if( !dryRunFlag ){ + dryRunFlag = find_option("nochange",0,0)!=0; /* deprecated */ + } + verboseFlag = find_option("verbose","v",0)!=0; + forceMissingFlag = find_option("force-missing",0,0)!=0; + debugFlag = find_option("debug",0,0)!=0; + setmtimeFlag = find_option("setmtime",0,0)!=0; + keepMergeFlag = find_option("keep-merge-files", "K",0)!=0; + bNosync = find_option("nosync",0,0)!=0; + + /* We should be done with options.. */ + verify_all_options(); + + db_must_be_within_tree(); + vid = db_lget_int("checkout", 0); + zCurBrName = branch_of_rid(vid); + user_select(); + if( !dryRunFlag && !internalUpdate && !bNosync ){ + if( autosync_loop(SYNC_PULL + SYNC_VERBOSE*verboseFlag, 1, "update") ){ + fossil_fatal("update abandoned due to sync failure"); + } + } + + /* Create any empty directories now, as well as after the update, + ** so changes in settings are reflected now */ + if( !dryRunFlag ) ensure_empty_dirs_created(0); + + if( internalUpdate ){ + tid = internalUpdate; + }else if( g.argc>=3 ){ + if( fossil_strcmp(g.argv[2], "current")==0 ){ + /* If VERSION is "current", then use the same algorithm to find the + ** target as if VERSION were omitted. */ + }else if( fossil_strcmp(g.argv[2], "latest")==0 ){ + /* If VERSION is "latest", then use the same algorithm to find the + ** target as if VERSION were omitted and the --latest flag is present. + */ + latestFlag = 1; + }else{ + tid = name_to_typed_rid(g.argv[2],"ci"); + if( tid==0 || !is_a_version(tid) ){ + fossil_fatal("no such check-in: %s", g.argv[2]); + } + } + } + + /* If no VERSION is specified on the command-line, then look for a + ** descendant of the current version. If there are multiple descendants, + ** look for one from the same branch as the current version. If there + ** are still multiple descendants, show them all and refuse to update + ** until the user selects one. + */ + if( tid==0 ){ + int closeCode = 1; + compute_leaves(vid, closeCode); + if( !db_exists("SELECT 1 FROM leaves") ){ + closeCode = 0; + compute_leaves(vid, closeCode); + } + if( !latestFlag && db_int(0, "SELECT count(*) FROM leaves")>1 ){ + db_multi_exec( + "DELETE FROM leaves WHERE rid NOT IN" + " (SELECT leaves.rid FROM leaves, tagxref" + " WHERE leaves.rid=tagxref.rid AND tagxref.tagid=%d" + " AND tagxref.value==(SELECT value FROM tagxref" + " WHERE tagid=%d AND rid=%d))", + TAG_BRANCH, TAG_BRANCH, vid + ); + if( db_int(0, "SELECT count(*) FROM leaves")>1 ){ + compute_leaves(vid, closeCode); + db_prepare(&q, + "%s " + " AND event.objid IN leaves" + " ORDER BY event.mtime DESC", + timeline_query_for_tty() + ); + print_timeline(&q, -100, width, 0, 0); + db_finalize(&q); + fossil_fatal("Multiple descendants"); + } + } + tid = db_int(0, "SELECT rid FROM leaves, event" + " WHERE event.objid=leaves.rid" + " ORDER BY event.mtime DESC"); + if( tid==0 ) tid = vid; + } + + if( tid==0 ){ + return; + } + + db_begin_transaction(); + db_multi_exec( + "CREATE TEMP TABLE dir_to_delete(name TEXT %s PRIMARY KEY)WITHOUT ROWID", + filename_collation() + ); + vfile_check_signature(vid, CKSIG_ENOTFILE); + if( !dryRunFlag && !internalUpdate ) undo_begin(); + if( load_vfile_from_rid(tid) && !forceMissingFlag ){ + fossil_fatal("missing content, unable to update"); + }; + + /* + ** The record.fn field is used to match files against each other. The + ** FV table contains one row for each unique filename in + ** in the current check-out, the pivot, and the version being merged. + */ + db_multi_exec( + "DROP TABLE IF EXISTS fv;" + "CREATE TEMP TABLE fv(" + " fn TEXT %s PRIMARY KEY," /* The filename relative to root */ + " idv INTEGER," /* VFILE entry for current version */ + " idt INTEGER," /* VFILE entry for target version */ + " chnged BOOLEAN," /* True if current version has been edited */ + " islinkv BOOLEAN," /* True if current file is a link */ + " islinkt BOOLEAN," /* True if target file is a link */ + " ridv INTEGER," /* Record ID for current version */ + " ridt INTEGER," /* Record ID for target */ + " isexe BOOLEAN," /* Does target have execute permission? */ + " deleted BOOLEAN DEFAULT 0,"/* File marked by "rm" to become unmanaged */ + " fnt TEXT %s" /* Filename of same file on target version */ + ");", + filename_collation(), filename_collation() + ); + + /* Add files found in the current version + */ + db_multi_exec( + "INSERT OR IGNORE INTO fv(fn,fnt,idv,idt,ridv,ridt,isexe,chnged,deleted)" + " SELECT pathname, pathname, id, 0, rid, 0, isexe, chnged, deleted" + " FROM vfile WHERE vid=%d", + vid + ); + + /* Compute file name changes on V->T. Record name changes in files that + ** have changed locally. + */ + if( vid ){ + find_filename_changes(vid, tid, 1, &nChng, &aChng, debugFlag ? "V->T": 0); + if( nChng ){ + for(i=0; i<nChng; i++){ + db_multi_exec( + "UPDATE fv" + " SET fnt=(SELECT name FROM filename WHERE fnid=%d)" + " WHERE fn=(SELECT name FROM filename WHERE fnid=%d) AND chnged", + aChng[i*2+1], aChng[i*2] + ); + } + fossil_free(aChng); + } + } + + /* Add files found in the target version T but missing from the current + ** version V. + */ + db_multi_exec( + "INSERT OR IGNORE INTO fv(fn,fnt,idv,idt,ridv,ridt,isexe,chnged)" + " SELECT pathname, pathname, 0, 0, 0, 0, isexe, 0 FROM vfile" + " WHERE vid=%d" + " AND pathname %s NOT IN (SELECT fnt FROM fv)", + tid, filename_collation() + ); + + /* + ** Compute the file version ids for T + */ + db_multi_exec( + "UPDATE fv SET" + " idt=coalesce((SELECT id FROM vfile WHERE vid=%d AND fnt=pathname),0)," + " ridt=coalesce((SELECT rid FROM vfile WHERE vid=%d AND fnt=pathname),0)", + tid, tid + ); + + /* + ** Add islink information + */ + db_multi_exec( + "UPDATE fv SET" + " islinkv=coalesce((SELECT islink FROM vfile" + " WHERE vid=%d AND fnt=pathname),0)," + " islinkt=coalesce((SELECT islink FROM vfile" + " WHERE vid=%d AND fnt=pathname),0)", + vid, tid + ); + + + if( debugFlag ){ + db_prepare(&q, + "SELECT rowid, fn, fnt, chnged, ridv, ridt, isexe," + " islinkv, islinkt FROM fv" + ); + while( db_step(&q)==SQLITE_ROW ){ + fossil_print("%3d: ridv=%-4d ridt=%-4d chnged=%d isexe=%d" + " islinkv=%d islinkt=%d\n", + db_column_int(&q, 0), + db_column_int(&q, 4), + db_column_int(&q, 5), + db_column_int(&q, 3), + db_column_int(&q, 6), + db_column_int(&q, 7), + db_column_int(&q, 8)); + fossil_print(" fnv = [%s]\n", db_column_text(&q, 1)); + fossil_print(" fnt = [%s]\n", db_column_text(&q, 2)); + } + db_finalize(&q); + } + + /* If FILES appear on the command-line, remove from the "fv" table + ** every entry that is not named on the command-line or which is not + ** in a directory named on the command-line. + */ + if( g.argc>=4 ){ + Blob sql; /* SQL statement to purge unwanted entries */ + Blob treename; /* Normalized filename */ + int i; /* Loop counter */ + const char *zSep; /* Term separator */ + + blob_zero(&sql); + blob_append(&sql, "DELETE FROM fv WHERE ", -1); + zSep = ""; + for(i=3; i<g.argc; i++){ + file_tree_name(g.argv[i], &treename, 0, 1); + if( file_isdir(g.argv[i], RepoFILE)==1 ){ + if( blob_size(&treename) != 1 || blob_str(&treename)[0] != '.' ){ + blob_append_sql(&sql, "%sfn NOT GLOB '%q/*' ", + zSep /*safe-for-%s*/, blob_str(&treename)); + }else{ + blob_reset(&sql); + break; + } + }else{ + blob_append_sql(&sql, "%sfn<>%Q ", + zSep /*safe-for-%s*/, blob_str(&treename)); + } + zSep = "AND "; + blob_reset(&treename); + } + db_multi_exec("%s", blob_sql_text(&sql)); + blob_reset(&sql); + } + + /* + ** Alter the content of the check-out so that it conforms with the + ** target + */ + db_prepare(&q, + "SELECT fn, idv, ridv, idt, ridt, chnged, fnt," + " isexe, islinkv, islinkt, deleted FROM fv ORDER BY 1" + ); + db_prepare(&mtimeXfer, + "UPDATE vfile SET mtime=(SELECT mtime FROM vfile WHERE id=:idv)" + " WHERE id=:idt" + ); + assert( g.zLocalRoot!=0 ); + assert( strlen(g.zLocalRoot)>0 ); + assert( g.zLocalRoot[strlen(g.zLocalRoot)-1]=='/' ); + merge_info_init(); + while( db_step(&q)==SQLITE_ROW ){ + const char *zName = db_column_text(&q, 0); /* The filename from root */ + int idv = db_column_int(&q, 1); /* VFILE entry for current */ + int ridv = db_column_int(&q, 2); /* RecordID for current */ + int idt = db_column_int(&q, 3); /* VFILE entry for target */ + int ridt = db_column_int(&q, 4); /* RecordID for target */ + int chnged = db_column_int(&q, 5); /* Current is edited */ + const char *zNewName = db_column_text(&q,6);/* New filename */ + int isexe = db_column_int(&q, 7); /* EXE perm for new file */ + int islinkv = db_column_int(&q, 8); /* Is current file is a link */ + int islinkt = db_column_int(&q, 9); /* Is target file is a link */ + int deleted = db_column_int(&q, 10); /* Marked for deletion */ + char *zFullPath; /* Full pathname of the file */ + char *zFullNewPath; /* Full pathname of dest */ + char nameChng; /* True if the name changed */ + const char *zOp = 0; /* Type of change. */ + i64 sz = 0; /* Size of the file */ + int nc = 0; /* Number of conflicts */ + const char *zErrMsg = 0; /* Error message */ + + zFullPath = mprintf("%s%s", g.zLocalRoot, zName); + zFullNewPath = mprintf("%s%s", g.zLocalRoot, zNewName); + sz = file_size(zFullNewPath, ExtFILE); + nameChng = fossil_strcmp(zName, zNewName); + nUpdate++; + if( deleted ){ + db_multi_exec("UPDATE vfile SET deleted=1 WHERE id=%d", idt); + } + if( idv>0 && ridv==0 && idt>0 && ridt>0 ){ + /* Conflict. This file has been added to the current check-out + ** but also exists in the target check-out. Use the current version. + */ + fossil_print("CONFLICT %s\n", zName); + nConflict++; + zOp = "CONFLICT"; + nc = 1; + zErrMsg = "duplicate file"; + }else if( idt>0 && idv==0 ){ + /* File added in the target. */ + if( file_isfile_or_link(zFullPath) ){ + /* Name of backup file with Original content */ + char *zOrig = file_newname(zFullPath, "original", 1); + /* Backup previously unmanaged file before being overwritten */ + file_copy(zFullPath, zOrig); + fossil_print("ADD %s - overwrites an unmanaged file", zName); + if( !dryRunFlag ){ + fossil_print(", original copy backed up as \"%s\"", zOrig); + file_delete(zFullPath); + } + fossil_print("\n"); + fossil_free(zOrig); + nOverwrite++; + nc = 1; + zOp = "CONFLICT"; + zErrMsg = "new file overwrites unmanaged file"; + }else{ + fossil_print("ADD %s\n", zName); + } + if( !dryRunFlag && !internalUpdate ) undo_save(zName); + if( !dryRunFlag ) vfile_to_disk(0, idt, 0, 0); + }else if( idt>0 && idv>0 && ridt!=ridv && (chnged==0 || deleted) ){ + /* The file is unedited. Change it to the target version */ + if( deleted ){ + fossil_print("UPDATE %s - change to unmanaged file\n", zName); + }else{ + fossil_print("UPDATE %s\n", zName); + } + if( !dryRunFlag && !internalUpdate ) undo_save(zName); + if( !dryRunFlag ) vfile_to_disk(0, idt, 0, 0); + zOp = "UPDATE"; + }else if( idt>0 && idv>0 && !deleted && file_size(zFullPath, RepoFILE)<0 ){ + /* The file missing from the local check-out. Restore it to the + ** version that appears in the target. */ + fossil_print("UPDATE %s\n", zName); + if( !dryRunFlag && !internalUpdate ) undo_save(zName); + if( !dryRunFlag ) vfile_to_disk(0, idt, 0, 0); + zOp = "UPDATE"; + }else if( idt==0 && idv>0 ){ + if( ridv==0 ){ + /* Added in current check-out. Continue to hold the file as + ** as an addition */ + db_multi_exec("UPDATE vfile SET vid=%d WHERE id=%d", tid, idv); + }else if( chnged ){ + /* Edited locally but deleted from the target. Do not track the + ** file but keep the edited version around. */ + fossil_print("CONFLICT %s - edited locally but deleted by update\n", + zName); + zOp = "CONFLICT"; + zErrMsg = "edited locally but deleted by update"; + nc = 1; + nConflict++; + }else{ + fossil_print("REMOVE %s\n", zName); + if( !dryRunFlag && !internalUpdate ) undo_save(zName); + if( !dryRunFlag ){ + char *zDir; + file_delete(zFullPath); + zDir = file_dirname(zName); + while( zDir!=0 ){ + char *zNext; + db_multi_exec("INSERT OR IGNORE INTO dir_to_delete(name)" + "VALUES(%Q)", zDir); + zNext = db_changes() ? file_dirname(zDir) : 0; + fossil_free(zDir); + zDir = zNext; + } + } + } + }else if( idt>0 && idv>0 && ridt!=ridv && chnged ){ + if( nameChng ){ + fossil_print("MERGE %s -> %s\n", zName, zNewName); + }else{ + fossil_print("MERGE %s\n", zName); + } + if( islinkv || islinkt ){ + fossil_print("***** Cannot merge symlink %s\n", zNewName); + zOp = "CONFLICT"; + nConflict++; + }else{ + /* Merge the changes in the current tree into the target version */ + Blob r, t, v; + int rc; + unsigned mergeFlags = dryRunFlag ? MERGE_DRYRUN : 0; + if(keepMergeFlag!=0) mergeFlags |= MERGE_KEEP_FILES; + if( !dryRunFlag && !internalUpdate ) undo_save(zName); + content_get(ridt, &t); + content_get(ridv, &v); + rc = merge_3way(&v, zFullPath, &t, &r, mergeFlags); + if( rc>=0 ){ + if( !dryRunFlag ){ + blob_write_to_file(&r, zFullNewPath); + file_setexe(zFullNewPath, isexe); + } + if( rc>0 ){ + nc = rc; + zOp = "CONFLICT"; + zErrMsg = "merge conflicts"; + fossil_print("***** %d merge conflicts in %s\n", rc, zNewName); + nConflict++; + }else{ + zOp = "MERGE"; + } + }else{ + if( !dryRunFlag ){ + if( !keepMergeFlag ){ + /* Name of backup file with Original content */ + char *zOrig = file_newname(zFullPath, "original", 1); + /* Backup non-mergeable binary file when --keep-merge-files is + not specified */ + file_copy(zFullPath, zOrig); + fossil_free(zOrig); + } + blob_write_to_file(&t, zFullNewPath); + file_setexe(zFullNewPath, isexe); + } + fossil_print("***** Cannot merge binary file %s", zNewName); + if( !dryRunFlag ){ + fossil_print(", original copy backed up locally"); + } + fossil_print("\n"); + nConflict++; + zOp = "ERROR"; + zErrMsg = "cannot merge binary file"; + nc = 1; + } + blob_reset(&v); + blob_reset(&t); + blob_reset(&r); + } + if( nameChng && !dryRunFlag ) file_delete(zFullPath); + }else{ + nUpdate--; + if( chnged ){ + if( verboseFlag ) fossil_print("EDITED %s\n", zName); + }else{ + db_bind_int(&mtimeXfer, ":idv", idv); + db_bind_int(&mtimeXfer, ":idt", idt); + db_step(&mtimeXfer); + db_reset(&mtimeXfer); + if( verboseFlag ) fossil_print("UNCHANGED %s\n", zName); + } + } + if( zOp!=0 ){ + db_multi_exec( + "INSERT INTO mergestat(op,fnp,ridp,fn,ridv,sz,fnm,ridm,fnr,nc,msg)" + "VALUES(%Q,%Q,%d,%Q,NULL,%lld,%Q,%d,%Q,%d,%Q)", + /* op */ zOp, + /* fnp */ zName, + /* ridp */ ridv, + /* fn */ zNewName, + /* sz */ sz, + /* fnm */ zName, + /* ridm */ ridt, + /* fnr */ zNewName, + /* nc */ nc, + /* msg */ zErrMsg + ); + } + free(zFullPath); + free(zFullNewPath); + } + db_finalize(&q); + db_finalize(&mtimeXfer); + fossil_print("%.79c\n",'-'); + zNewBrName = branch_of_rid(tid); + if( g.argc<3 && fossil_strcmp(zCurBrName, zNewBrName)!=0 ){ + zBrChgMsg = mprintf(" Branch changed from %s to %s.", + zCurBrName, zNewBrName); + } + if( nUpdate==0 ){ + show_common_info(tid, "checkout:", 1, 0); + fossil_print("%-13s None. Already up-to-date.%s\n", "changes:", zBrChgMsg); + }else{ + fossil_print("%-13s %.40s %s\n", "updated-from:", rid_to_uuid(vid), + db_text("", "SELECT datetime(mtime) || ' UTC' FROM event " + " WHERE objid=%d", vid)); + show_common_info(tid, "updated-to:", 1, 0); + fossil_print("%-13s %d file%s modified.%s\n", "changes:", + nUpdate, nUpdate>1 ? "s" : "", zBrChgMsg); + } + + /* Report on conflicts + */ + if( !dryRunFlag ){ + Stmt q; + int nMerge = 0; + db_prepare(&q, "SELECT mhash, id FROM vmerge WHERE id<=0"); + while( db_step(&q)==SQLITE_ROW ){ + const char *zLabel = "merge"; + switch( db_column_int(&q, 1) ){ + case -1: zLabel = "cherrypick merge"; break; + case -2: zLabel = "backout merge"; break; + } + fossil_warning("uncommitted %s against %S.", + zLabel, db_column_text(&q, 0)); + nMerge++; + } + db_finalize(&q); + leaf_ambiguity_warning(tid, tid); + + if( nConflict ){ + if( internalUpdate ){ + internalConflictCnt = nConflict; + nConflict = 0; + }else{ + fossil_warning("WARNING: %d merge conflicts", nConflict); + } + } + if( nOverwrite ){ + fossil_warning("WARNING: %d unmanaged files were overwritten", + nOverwrite); + } + if( nMerge ){ + fossil_warning("WARNING: %d uncommitted prior merges", nMerge); + } + } + + /* + ** Clean up the mid and pid VFILE entries. Then commit the changes. + */ + if( dryRunFlag ){ + db_end_transaction(1); /* With --dry-run, rollback changes */ + fossil_warning("\nREMINDER: this was a dry run -" + " no files were actually changed " + "(checkout is still %.10s).", rid_to_uuid(vid)); + }else{ + char *zPwd; + ensure_empty_dirs_created(1); + sqlite3_create_function(g.db, "rmdir", 1, SQLITE_UTF8|SQLITE_DIRECTONLY, 0, + file_rmdir_sql_function, 0, 0); + zPwd = file_getcwd(0,0); + db_multi_exec( + "SELECT rmdir(%Q||name) FROM dir_to_delete" + " WHERE (%Q||name)<>%Q ORDER BY name DESC", + g.zLocalRoot, g.zLocalRoot, zPwd + ); + fossil_free(zPwd); + if( g.argc<=3 ){ + /* All files updated. Shift the current check-out to the target. */ + db_multi_exec("DELETE FROM vfile WHERE vid!=%d", tid); + checkout_set_all_exe(tid); + db_set_checkout(tid, 1); + }else{ + /* A subset of files have been checked out. Keep the current + ** check-out unchanged. */ + db_multi_exec("DELETE FROM vfile WHERE vid!=%d", vid); + } + if( !internalUpdate ) undo_finish(); + if( setmtimeFlag ) vfile_check_signature(tid, CKSIG_SETMTIME); + db_end_transaction(0); + } +} + +/* +** Create empty directories specified by the empty-dirs setting. +*/ +void ensure_empty_dirs_created(int clearDirTable){ + char *zEmptyDirs = db_get("empty-dirs", 0); + if( zEmptyDirs!=0 ){ + int i; + Glob *pGlob = glob_create(zEmptyDirs); + + for(i=0; pGlob!=0 && i<pGlob->nPattern; i++){ + const char *zDir = pGlob->azPattern[i]; + char *zPath = mprintf("%s/%s", g.zLocalRoot, zDir); + switch( file_isdir(zPath, RepoFILE) ){ + case 0: { /* doesn't exist */ + fossil_free(zPath); + zPath = mprintf("%s/%s/x", g.zLocalRoot, zDir); + if( file_mkfolder(zPath, RepoFILE, 0, 1)!=0 ) { + fossil_warning("couldn't create directory %s as " + "required by empty-dirs setting", zDir); + } + break; + } + case 1: { /* exists, and is a directory */ + /* make sure this directory is not on the delete list */ + if( clearDirTable ){ + db_multi_exec( + "DELETE FROM dir_to_delete WHERE name=%Q", zDir + ); + } + break; + } + case 2: { /* exists, but isn't a directory */ + fossil_warning("file %s found, but a directory is required " + "by empty-dirs setting", zDir); + } + } + fossil_free(zPath); + } + glob_free(pGlob); + } +} + +/* +** Get the manifest record for a given revision, or the current check-out if +** zRevision is NULL. +*/ +Manifest *historical_manifest( + const char *zRevision /* The check-in to query, or NULL for current */ +){ + int vid; + Manifest *pManifest; + + /* Determine the check-in manifest artifact ID. Panic on failure. */ + if( zRevision ){ + vid = name_to_typed_rid(zRevision, "ci"); + }else if( !g.localOpen ){ + vid = name_to_typed_rid(db_main_branch(), "ci"); + }else{ + vid = db_lget_int("checkout", 0); + if( !is_a_version(vid) ){ + if( vid==0 ) return 0; + zRevision = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid); + if( zRevision ){ + fossil_fatal("check-out artifact is not a check-in: %s", zRevision); + }else{ + fossil_fatal("invalid check-out artifact ID: %d", vid); + } + } + } + + /* Parse the manifest, given its artifact ID. Panic on failure. */ + if( !(pManifest = manifest_get(vid, CFTYPE_MANIFEST, 0)) ){ + if( zRevision ){ + fossil_fatal("could not parse manifest for check-in: %s", zRevision); + }else{ + fossil_fatal("could not parse manifest for current check-out"); + } + } + + /* Return the manifest pointer. The caller must use manifest_destroy() to + * clean up when finished using the manifest. */ + return pManifest; +} + +/* +** Get the contents of a file within the check-in "zRevision". If +** zRevision==NULL then get the file content for the current check-out. +*/ +int historical_blob( + const char *zRevision, /* The check-in containing the file */ + const char *zFile, /* Full treename of the file */ + Blob *pBlob, /* Put the content here */ + int fatal /* If nonzero, panic if file/artifact not found */ +){ + int result = 0; + + /* Get the manifest for the requested check-in version. This call unavoidably + * panics on failure even if fatal is not set. */ + Manifest *pManifest = historical_manifest(zRevision); + + /* Try to find the file record within the manifest. */ + ManifestFile *pFile = manifest_file_find(pManifest, zFile); + + if( !pFile ){ + /* Process file-not-found errors. */ + if( fatal ){ + if( zRevision ){ + fossil_fatal("file %s does not exist in check-in %s", zFile, zRevision); + }else{ + fossil_fatal("no such file: %s", zFile); + } + } + }else{ + /* Get the file's contents. */ + result = content_get(fast_uuid_to_rid(pFile->zUuid), pBlob); + + /* Process artifact-not-found errors. */ + if( !result && fatal ){ + if( zRevision ){ + fossil_fatal("missing artifact %s for file %s in check-in %s", + pFile->zUuid, zFile, zRevision); + }else{ + fossil_fatal("missing artifact %s for file %s", pFile->zUuid, zFile); + } + } + } + + /* Deallocate the parsed manifest structure. */ + manifest_destroy(pManifest); + + /* Return 1 on success and (assuming fatal is not set) 0 if not found. */ + return result; +} + +/* +** COMMAND: revert +** +** Usage: %fossil revert ?OPTIONS? ?FILE ...? +** +** Revert to the current repository version of FILE, or to +** the baseline VERSION specified with -r flag. +** +** If FILE was part of a rename operation, both the original file +** and the renamed file are reverted. +** +** Using a directory name for any of the FILE arguments is the same +** as using every subdirectory and file beneath that directory. +** +** Revert all files if no file name is provided. +** +** If a file is reverted accidentally, it can be restored using +** the "fossil undo" command. +** +** Options: +** --noundo Do not record changes in the undo/redo log. +** -r|--revision VERSION Revert given FILE(s) back to given +** VERSION +** +** See also: [[redo]], [[undo]], [[checkout]], [[update]] +*/ +void revert_cmd(void){ + Manifest *pCoManifest; /* Manifest of current check-out */ + Manifest *pRvManifest; /* Manifest of selected revert version */ + ManifestFile *pCoFile; /* File within current check-out manifest */ + ManifestFile *pRvFile; /* File within revert version manifest */ + const char *zFile; /* Filename relative to check-out root */ + const char *zRevision; /* Selected revert version, NULL if current */ + Blob record = BLOB_INITIALIZER; /* Contents of each reverted file */ + int useUndo = 1; /* True to record changes in UNDO */ + int i; + Stmt q; + int revertAll = 0; + int revisionOptNotSupported = 0; + + undo_capture_command_line(); + zRevision = find_option("revision", "r", 1); + useUndo = find_option("noundo", 0, 0)==0; + verify_all_options(); + + if( g.argc<2 ){ + usage("?OPTIONS? [FILE] ..."); + } + if( zRevision && g.argc<3 ){ + fossil_fatal("directories or the entire tree can only be reverted" + " back to current version"); + } + db_must_be_within_tree(); + + /* Get manifests of revert version and (if different) current check-out. */ + pRvManifest = historical_manifest(zRevision); + pCoManifest = zRevision ? historical_manifest(0) : 0; + + db_begin_transaction(); + if( useUndo ){ + undo_begin(); + }else{ + undo_reset(); + } + db_multi_exec("CREATE TEMP TABLE torevert(name UNIQUE);"); + + if( g.argc>2 ){ + for(i=2; i<g.argc; i++){ + Blob fname; + zFile = mprintf("%/", g.argv[i]); + blob_zero(&fname); + file_tree_name(zFile, &fname, 0, 1); + if( blob_eq(&fname, ".") ){ + if( zRevision ){ + revisionOptNotSupported = 1; + break; + } + revertAll = 1; + break; + }else if( db_exists( + "SELECT pathname" + " FROM vfile" + " WHERE (substr(pathname,1,length('%q/'))='%q/'" + " OR substr(origname,1,length('%q/'))='%q/');", + blob_str(&fname), blob_str(&fname), + blob_str(&fname), blob_str(&fname)) ){ + int vid; + vid = db_lget_int("checkout", 0); + vfile_check_signature(vid, 0); + + if( zRevision ){ + revisionOptNotSupported = 1; + break; + } + db_multi_exec( + "INSERT OR IGNORE INTO torevert" + " SELECT pathname" + " FROM vfile" + " WHERE (substr(pathname,1,length('%q/'))='%q/'" + " OR substr(origname,1,length('%q/'))='%q/')" + " AND (chnged OR deleted OR rid=0 OR pathname!=origname);", + blob_str(&fname), blob_str(&fname), + blob_str(&fname), blob_str(&fname) + ); + }else{ + db_multi_exec( + "REPLACE INTO torevert VALUES(%B);" + "INSERT OR IGNORE INTO torevert" + " SELECT pathname" + " FROM vfile" + " WHERE origname=%B;", + &fname, &fname + ); + } + blob_reset(&fname); + } + }else{ + revertAll = 1; + } + + if( revisionOptNotSupported ){ + fossil_fatal("directories or the entire tree can only be reverted" + " back to current version"); + } + + if ( revertAll ){ + int vid; + vid = db_lget_int("checkout", 0); + vfile_check_signature(vid, 0); + db_multi_exec( + "DELETE FROM vmerge;" + "INSERT OR IGNORE INTO torevert " + " SELECT pathname" + " FROM vfile " + " WHERE chnged OR deleted OR rid=0 OR pathname!=origname;" + ); + } + + db_multi_exec( + "INSERT OR IGNORE INTO torevert" + " SELECT origname" + " FROM vfile" + " WHERE origname!=pathname AND pathname IN (SELECT name FROM torevert);" + ); + blob_zero(&record); + db_prepare(&q, "SELECT name FROM torevert"); + if( zRevision==0 ){ + int vid = db_lget_int("checkout", 0); + zRevision = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", vid); + } + while( db_step(&q)==SQLITE_ROW ){ + char *zFull; + zFile = db_column_text(&q, 0); + zFull = mprintf("%/%/", g.zLocalRoot, zFile); + pRvFile = pRvManifest? manifest_file_find(pRvManifest, zFile) : 0; + if( !pRvFile ){ + if( db_int(0, "SELECT rid FROM vfile WHERE pathname=%Q OR origname=%Q", + zFile, zFile)==0 ){ + fossil_print("UNMANAGE %s\n", zFile); + }else{ + if( useUndo ) undo_save(zFile); + file_delete(zFull); + fossil_print("DELETE %s\n", zFile); + } + db_multi_exec( + "UPDATE OR REPLACE vfile" + " SET pathname=origname, origname=NULL" + " WHERE pathname=%Q AND origname!=pathname;" + "DELETE FROM vfile WHERE pathname=%Q", + zFile, zFile + ); + }else if( file_unsafe_in_tree_path(zFull) ){ + /* Ignore this file */ + }else{ + sqlite3_int64 mtime; + int rvChnged = 0; + int rvPerm = manifest_file_mperm(pRvFile); + + /* Determine if reverted-to file is different than checked-out file. */ + if( pCoManifest && (pCoFile = manifest_file_find(pCoManifest, zFile)) ){ + rvChnged = manifest_file_mperm(pRvFile)!=rvPerm + || fossil_strcmp(pRvFile->zUuid, pCoFile->zUuid)!=0; + } + + /* Get contents of reverted-to file. */ + content_get(fast_uuid_to_rid(pRvFile->zUuid), &record); + + if( useUndo ) undo_save(zFile); + if( file_size(zFull, RepoFILE)>=0 + && (rvPerm==PERM_LNK || file_islink(0)) + ){ + file_delete(zFull); + } + if( rvPerm==PERM_LNK ){ + symlink_create(blob_str(&record), zFull); + }else{ + blob_write_to_file(&record, zFull); + } + file_setexe(zFull, rvPerm==PERM_EXE); + fossil_print("REVERT %s\n", zFile); + mtime = file_mtime(zFull, RepoFILE); + db_multi_exec( + "UPDATE vfile" + " SET mtime=%lld, chnged=%d, deleted=0, isexe=%d, islink=%d," + " mrid=rid, mhash=NULL" + " WHERE pathname=%Q OR origname=%Q", + mtime, rvChnged, rvPerm==PERM_EXE, rvPerm==PERM_LNK, zFile, zFile + ); + } + blob_reset(&record); + free(zFull); + } + db_finalize(&q); + if( useUndo) undo_finish(); + db_end_transaction(0); + + /* Deallocate parsed manifest structures. */ + manifest_destroy(pRvManifest); + manifest_destroy(pCoManifest); +} |
