/* ** 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 ** checkout into a different version and switch to that version. */ #include "config.h" #include "update.h" #include /* ** Return true if artifact rid is a version */ int is_a_version(int rid){ return db_exists("SELECT 1 FROM plink WHERE cid=%d " "UNION ALL SELECT 1 FROM plink WHERE pid=%d", rid, rid); } /* ** COMMAND: update ** ** Usage: %fossil update ?VERSION? ?FILES...? ** ** Change the version of the current checkout to VERSION. Any uncommitted ** changes are retained and applied to the new checkout. ** ** 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. If FILES is omitted, all ** files in the current checkout are subject to be updated. ** ** The -n or --nochange 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 checkout or the repository. ** ** The -v or --verbose option prints status information about unchanged ** files in addition to those file that actually do change. */ 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 nochangeFlag; /* -n or --nochange. Do a dry run */ int verboseFlag; /* -v or --verbose. Output extra information */ url_proxy_options(); latestFlag = find_option("latest",0, 0)!=0; nochangeFlag = find_option("nochange","n",0)!=0; verboseFlag = find_option("verbose","v",0)!=0; db_must_be_within_tree(); vid = db_lget_int("checkout", 0); if( vid==0 ){ fossil_fatal("cannot find current version"); } if( db_exists("SELECT 1 FROM vmerge") ){ fossil_fatal("cannot update an uncommitted merge"); } if( !nochangeFlag ) autosync(AUTOSYNC_PULL); if( g.argc>=3 ){ if( 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( 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_rid(g.argv[2]); if( tid==0 ){ fossil_fatal("no such version: %s", g.argv[2]); }else if( !is_a_version(tid) ){ fossil_fatal("no such version: %s", g.argv[2]); } } } if( tid==0 ){ compute_leaves(vid, 1); if( !latestFlag && db_int(0, "SELECT count(*) FROM leaves")>1 ){ db_prepare(&q, "%s " " AND event.objid IN leaves" " ORDER BY event.mtime DESC", timeline_query_for_tty() ); print_timeline(&q, 100); 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"); } db_begin_transaction(); vfile_check_signature(vid, 1); if( !nochangeFlag ) undo_begin(); load_vfile_from_rid(tid); /* ** The record.fn field is used to match files against each other. The ** FV table contains one row for each each unique filename in ** in the current checkout, the pivot, and the version being merged. */ db_multi_exec( "DROP TABLE IF EXISTS fv;" "CREATE TEMP TABLE fv(" " fn TEXT 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 */ " ridv INTEGER," /* Record ID for current version */ " ridt INTEGER " /* Record ID for target */ ");" "INSERT OR IGNORE INTO fv" " SELECT pathname, 0, 0, 0, 0, 0 FROM vfile" ); db_prepare(&q, "SELECT id, pathname, rid FROM vfile" " WHERE vid=%d", tid ); while( db_step(&q)==SQLITE_ROW ){ int id = db_column_int(&q, 0); const char *fn = db_column_text(&q, 1); int rid = db_column_int(&q, 2); db_multi_exec( "UPDATE fv SET idt=%d, ridt=%d WHERE fn=%Q", id, rid, fn ); } db_finalize(&q); db_prepare(&q, "SELECT id, pathname, rid, chnged FROM vfile" " WHERE vid=%d", vid ); while( db_step(&q)==SQLITE_ROW ){ int id = db_column_int(&q, 0); const char *fn = db_column_text(&q, 1); int rid = db_column_int(&q, 2); int chnged = db_column_int(&q, 3); db_multi_exec( "UPDATE fv SET idv=%d, ridv=%d, chnged=%d WHERE fn=%Q", id, rid, chnged, fn ); } 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. */ if( g.argc>=4 ){ Blob sql; /* SQL statement to purge unwanted entries */ char *zSep = "("; /* Separator in the list of filenames */ Blob treename; /* Normalized filename */ int i; /* Loop counter */ blob_zero(&sql); blob_append(&sql, "DELETE FROM fv WHERE fn NOT IN ", -1); for(i=3; i1 ); assert( g.zLocalRoot[strlen(g.zLocalRoot)-1]=='/' ); 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 */ char *zFullPath; /* Full pathname of the file */ zFullPath = mprintf("%s%s", g.zLocalRoot, zName); if( idv>0 && ridv==0 && idt>0 && ridt>0 ){ /* Conflict. This file has been added to the current checkout ** but also exists in the target checkout. Use the current version. */ printf("CONFLICT %s\n", zName); }else if( idt>0 && idv==0 ){ /* File added in the target. */ printf("ADD %s\n", zName); undo_save(zName); if( !nochangeFlag ) vfile_to_disk(0, idt, 0, 0); }else if( idt>0 && idv>0 && ridt!=ridv && chnged==0 ){ /* The file is unedited. Change it to the target version */ printf("UPDATE %s\n", zName); undo_save(zName); if( !nochangeFlag ) vfile_to_disk(0, idt, 0, 0); }else if( idt>0 && idv>0 && file_size(zFullPath)<0 ){ /* The file missing from the local check-out. Restore it to the ** version that appears in the target. */ printf("UPDATE %s\n", zName); undo_save(zName); if( !nochangeFlag ) vfile_to_disk(0, idt, 0, 0); }else if( idt==0 && idv>0 ){ if( ridv==0 ){ /* Added in current checkout. 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. Delete it. */ printf("CONFLICT %s\n", zName); }else{ char *zFullPath; printf("REMOVE %s\n", zName); undo_save(zName); zFullPath = mprintf("%s/%s", g.zLocalRoot, zName); if( !nochangeFlag ) unlink(zFullPath); free(zFullPath); } }else if( idt>0 && idv>0 && ridt!=ridv && chnged ){ /* Merge the changes in the current tree into the target version */ Blob e, r, t, v; int rc; printf("MERGE %s\n", zName); undo_save(zName); content_get(ridt, &t); content_get(ridv, &v); blob_zero(&e); blob_read_from_file(&e, zFullPath); rc = blob_merge(&v, &e, &t, &r); if( rc>=0 ){ if( !nochangeFlag ) blob_write_to_file(&r, zFullPath); if( rc>0 ){ printf("***** %d merge conflicts in %s\n", rc, zName); } }else{ printf("***** Cannot merge binary file %s\n", zName); } blob_reset(&v); blob_reset(&e); blob_reset(&t); blob_reset(&r); }else if( verboseFlag ){ printf("UNCHANGED %s\n", zName); } free(zFullPath); } db_finalize(&q); /* ** Clean up the mid and pid VFILE entries. Then commit the changes. */ if( nochangeFlag ){ db_end_transaction(1); /* With --nochange, rollback changes */ }else{ if( g.argc<=3 ){ /* All files updated. Shift the current checkout to the target. */ db_multi_exec("DELETE FROM vfile WHERE vid!=%d", tid); manifest_to_disk(tid); db_lset_int("checkout", tid); }else{ /* A subset of files have been checked out. Keep the current ** checkout unchanged. */ db_multi_exec("DELETE FROM vfile WHERE vid!=%d", vid); } undo_finish(); db_end_transaction(0); } } /* ** Get the contents of a file within the checking "revision". If ** revision==NULL then get the file content for the current checkout. */ int historical_version_of_file( const char *revision, /* The checkin containing the file */ const char *file, /* Full treename of the file */ Blob *content, /* Put the content here */ int errCode /* Error code if file not found. Panic if 0. */ ){ Blob mfile; Manifest m; int i, rid=0; if( revision ){ rid = name_to_rid(revision); }else{ rid = db_lget_int("checkout", 0); } if( !is_a_version(rid) ){ if( errCode>0 ) return errCode; fossil_fatal("no such checkin: %s", revision); } content_get(rid, &mfile); if( manifest_parse(&m, &mfile) ){ for(i=0; i2 ){ for(i=2; i