/* ** 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 push, pull, and sync a repository */ #include "config.h" #include "sync.h" #include /* ** Explain what type of sync operation is about to occur */ static void sync_explain(unsigned syncFlags){ if( g.url.isAlias ){ const char *url; if( g.url.useProxy ){ url = g.url.proxyUrlCanonical; }else{ url = g.url.canonical; } if( (syncFlags & (SYNC_PUSH|SYNC_PULL))==(SYNC_PUSH|SYNC_PULL) ){ fossil_print("Sync with %s\n", url); }else if( syncFlags & SYNC_PUSH ){ fossil_print("Push to %s\n", url); }else if( syncFlags & SYNC_PULL ){ fossil_print("Pull from %s\n", url); } } } /* ** Call client_sync() one or more times in order to complete a ** sync operation. Usually, client_sync() is called only once, though ** is can be called multiple times if the SYNC_ALLURL flags is set. */ static int client_sync_all_urls( unsigned syncFlags, /* Mask of SYNC_* flags */ unsigned configRcvMask, /* Receive these configuration items */ unsigned configSendMask, /* Send these configuration items */ const char *zAltPCode /* Alternative project code (usually NULL) */ ){ int nErr; int nOther; char **azOther; int i; Stmt q; sync_explain(syncFlags); nErr = client_sync(syncFlags, configRcvMask, configSendMask, zAltPCode); if( nErr==0 ) url_remember(); if( (syncFlags & SYNC_ALLURL)==0 ) return nErr; nOther = 0; azOther = 0; db_prepare(&q, "SELECT substr(name,10) FROM config" " WHERE name glob 'sync-url:*'" " AND value<>(SELECT value FROM config WHERE name='last-sync-url')" ); while( db_step(&q)==SQLITE_ROW ){ const char *zUrl = db_column_text(&q, 0); azOther = fossil_realloc(azOther, sizeof(*azOther)*(nOther+1)); azOther[nOther++] = fossil_strdup(zUrl); } db_finalize(&q); for(i=0; i fossil remote ** ** With no arguments, this command shows the current default remote ** URL. If there is no default, it shows "off". ** ** > fossil remote add NAME URL ** ** Add a new named URL. Afterwards, NAME can be used as a short ** symbolic name for URL in contexts where a URL is required. The ** URL argument can be "default" or a prior symbolic name to make ** a copy of an existing URL under the new NAME. The "default" ** remote cannot be defined with this subcommand; instead, ** use 'fossil remote REF' as documented below. ** ** > fossil remote config-data ** ** DEBUG USE ONLY - Show the name and value of every CONFIG table ** entry in the repository that is associated with the remote URL store. ** Passwords are obscured in the output. ** ** > fossil remote delete NAME ** ** Delete a named URL previously created by the "add" subcommand. ** ** > fossil remote hyperlink ?FILENAME? ?LINENUM? ?LINENUM? ** ** Print a URL that will access the current check-out on the remote ** repository. Or if the FILENAME argument is included, print the ** URL to access that particular file within the current check-out. ** If one or two linenumber arguments are provided after the filename, ** then the URL is for the line or range of lines specified. ** ** > fossil remote list|ls ** ** Show all remote repository URLs. ** ** > fossil remote off ** ** Forget the default URL. This disables autosync. ** ** This is a convenient way to enter "airplane mode". To enter ** airplane mode, first save the current default URL, then turn the ** default off. Perhaps like this: ** ** fossil remote add main default ** fossil remote off ** ** To exit airplane mode and turn autosync back on again: ** ** fossil remote main ** ** > fossil remote scrub ** ** Forget any saved passwords for remote repositories, but continue ** to remember the URLs themselves. You will be prompted for the ** password the next time it is needed. ** ** > fossil remote ui ?FILENAME? ?LINENUM? ?LINENUM? ** ** Bring up a web browser pointing at the remote repository, and ** specifically to the page that describes the current check-out ** on that remote repository. Or if FILENAME and/or LINENUM arguments ** are provided, to the specific file and range of lines. This ** command is similar to "fossil remote hyperlink" except that instead ** of printing the URL, it passes the URL off to the web browser. ** ** > fossil remote REF ** ** Make REF the new default URL, replacing the prior default. ** REF may be a URL or a NAME from a prior "add". */ void remote_url_cmd(void){ char *zUrl, *zArg; int nArg; db_find_and_open_repository(0, 0); /* We should be done with options.. */ verify_all_options(); /* 2021-10-25: A note about data structures. ** ** The remote URLs are stored in the CONFIG table. The URL is stored ** separately from the password. The password is obscured using the ** obscure() function. ** ** Originally, Fossil only preserved a single remote URL. That URL ** is stored in "last-sync-url" and the password in "last-sync-pw". The ** ability to have multiple remotes was added later so these names ** were retained for backwards compatibility. The other remotes are ** stored in "sync-url:NAME" and "sync-pw:NAME" where NAME is the name ** of the remote. ** ** The last-sync-url is called "default" for the display list. ** ** The last-sync-url might be duplicated into one of the sync-url:NAME ** entries. Thus, when doing a "fossil sync --all" or an autosync with ** autosync=all, each sync-url:NAME entry is checked to see if it is the ** same as last-sync-url and if it is then that entry is skipped. */ if( g.argc==2 ){ /* "fossil remote" with no arguments: Show the last sync URL. */ zUrl = db_get("last-sync-url", 0); if( zUrl==0 ){ fossil_print("off\n"); }else{ url_parse(zUrl, 0); fossil_print("%s\n", g.url.canonical); } return; } zArg = g.argv[2]; nArg = (int)strlen(zArg); if( strcmp(zArg,"off")==0 ){ /* fossil remote off ** Forget the last-sync-URL and its password */ if( g.argc!=3 ) usage("off"); remote_delete_default: db_unprotect(PROTECT_CONFIG); db_multi_exec( "DELETE FROM config WHERE name GLOB 'last-sync-*';" ); db_protect_pop(); return; } if( strncmp(zArg, "list", nArg)==0 || strcmp(zArg,"ls")==0 ){ Stmt q; if( g.argc!=3 ) usage("list"); db_prepare(&q, "SELECT 'default', value FROM config WHERE name='last-sync-url'" " UNION ALL " "SELECT substr(name,10), value FROM config" " WHERE name GLOB 'sync-url:*'" " ORDER BY 1" ); while( db_step(&q)==SQLITE_ROW ){ fossil_print("%-18s %s\n", db_column_text(&q,0), db_column_text(&q,1)); } db_finalize(&q); return; } if( strcmp(zArg, "add")==0 ){ char *zName; char *zUrl; UrlData x; if( g.argc!=5 ) usage("add NAME URL"); memset(&x, 0, sizeof(x)); zName = g.argv[3]; zUrl = g.argv[4]; if( strcmp(zName,"default")==0 ){ fossil_fatal("update the \"default\" remote-url with 'fossil remote REF'" "\nsee 'fossil help remote' for complete usage information"); } db_begin_write(); if( fossil_strcmp(zUrl,"default")==0 ){ x.canonical = db_get("last-sync-url",0); x.passwd = unobscure(db_get("last-sync-pw",0)); }else{ url_parse_local(zUrl, URL_PROMPT_PW|URL_USE_CONFIG, &x); } db_unprotect(PROTECT_CONFIG); db_multi_exec( "REPLACE INTO config(name, value, mtime)" " VALUES('sync-url:%q',%Q,now())", zName, x.canonical ); db_multi_exec( "REPLACE INTO config(name, value, mtime)" " VALUES('sync-pw:%q',obscure(%Q),now())", zName, x.passwd ); db_protect_pop(); db_commit_transaction(); return; } if( strncmp(zArg, "delete", nArg)==0 ){ char *zName; if( g.argc!=4 ) usage("delete NAME"); zName = g.argv[3]; if( strcmp(zName,"default")==0 ) goto remote_delete_default; db_begin_write(); db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name glob 'sync-url:%q'", zName); db_multi_exec("DELETE FROM config WHERE name glob 'sync-pw:%q'", zName); db_protect_pop(); db_commit_transaction(); return; } if( strncmp(zArg, "hyperlink", nArg)==0 || (nArg==2 && strcmp(zArg, "ui")==0) ){ char *zBase; char *zUuid; Blob fname; Blob url; char *zSubCmd = g.argv[2][0]=='u' ? "ui" : "hyperlink"; if( !db_table_exists("localdb","vvar") ){ fossil_fatal("the \"remote %s\" command only works from " "within an open check-out", zSubCmd); } zUrl = db_get("last-sync-url", 0); if( zUrl==0 ){ zUrl = "http://localhost:8080/"; } url_parse(zUrl, 0); if( g.url.isFile ){ url_parse("http://localhost:8080/", 0); } zBase = url_nouser(&g.url); blob_init(&url, 0, 0); if( g.argc==3 ){ blob_appendf(&url, "%s/info/%!S", zBase, db_text("???", "SELECT uuid FROM blob, vvar" " WHERE blob.rid=0+vvar.value" " AND vvar.name='checkout';" )); }else{ blob_init(&fname, 0, 0); file_tree_name(g.argv[3], &fname, 0, 1); zUuid = db_text(0, "SELECT uuid FROM files_of_checkin" " WHERE checkinID=(SELECT value FROM vvar WHERE name='checkout')" " AND filename=%Q", blob_str(&fname) ); if( zUuid==0 ){ fossil_fatal("not a managed file: \"%s\"", g.argv[3]); } blob_appendf(&url, "%s/info/%S",zBase,zUuid); if( g.argc>4 ){ int ln1 = atoi(g.argv[4]); if( ln1<=0 || sqlite3_strglob("*[^0-9]*",g.argv[4])==0 ){ fossil_fatal("\"%s\" is not a valid line number", g.argv[4]); } if( g.argc>5 ){ int ln2 = atoi(g.argv[5]); if( ln2==0 || sqlite3_strglob("*[^0-9]*",g.argv[5])==0 ){ fossil_fatal("\"%s\" is not a valid line number", g.argv[5]); } if( ln2<=ln1 ){ fossil_fatal("second line number should be greater than the first"); } blob_appendf(&url,"?ln=%d,%d", ln1, ln2); }else{ blob_appendf(&url,"?ln=%d", ln1); } } if( g.argc>6 ){ usage(mprintf("%s ?FILENAME? ?LINENUMBER? ?LINENUMBER?", zSubCmd)); } } if( g.argv[2][0]=='u' ){ char *zCmd; zCmd = mprintf("%s %!$ &", fossil_web_browser(), blob_str(&url)); fossil_system(zCmd); }else{ fossil_print("%s\n", blob_str(&url)); } return; } if( strncmp(zArg, "scrub", nArg)==0 ){ if( g.argc!=3 ) usage("scrub"); db_begin_write(); db_unprotect(PROTECT_CONFIG); db_multi_exec("DELETE FROM config WHERE name glob 'sync-pw:*'"); db_multi_exec("DELETE FROM config WHERE name = 'last-sync-pw'"); db_protect_pop(); db_commit_transaction(); return; } if( strncmp(zArg, "config-data", nArg)==0 ){ /* Undocumented command: "fossil remote config-data" ** ** Show the CONFIG table entries that relate to remembering remote URLs */ Stmt q; int n; n = db_int(13, "SELECT max(length(name))" " FROM config" " WHERE name GLOB 'sync-*:*'" " OR name GLOB 'last-sync-*'" " OR name GLOB 'parent-project-*'" ); db_prepare(&q, "SELECT name," " CASE WHEN name LIKE '%%sync-pw%%' OR name='parent-project-pw'" " THEN printf('%%.*c',length(value),'*') ELSE value END" " FROM config" " WHERE name GLOB 'sync-*:*'" " OR name GLOB 'last-sync-*'" " OR name GLOB 'parent-project-*'" " ORDER BY name LIKE '%%sync-pw%%' OR name='parent-project-pw', name" ); while( db_step(&q)==SQLITE_ROW ){ fossil_print("%-*s %s\n", n, db_column_text(&q,0), db_column_text(&q,1) ); } db_finalize(&q); return; } if( sqlite3_strlike("http://%",zArg,0)==0 || sqlite3_strlike("https://%",zArg,0)==0 || sqlite3_strlike("ssh:%",zArg,0)==0 || sqlite3_strlike("file:%",zArg,0)==0 || db_exists("SELECT 1 FROM config WHERE name='sync-url:%q'",zArg) ){ db_unset("last-sync-url", 0); db_unset("last-sync-pw", 0); url_parse(g.argv[2], URL_REMEMBER|URL_PROMPT_PW| URL_USE_CONFIG|URL_ASK_REMEMBER_PW); url_remember(); return; } fossil_fatal("unknown command \"%s\" - should be a URL or one of: " "add delete hyperlink list off scrub", zArg); } /* ** COMMAND: backup* ** ** Usage: %fossil backup ?OPTIONS? FILE|DIRECTORY ** ** Make a backup of the repository into the named file or into the named ** directory. This backup is guaranteed to be consistent even if there are ** concurrent changes taking place on the repository. In other words, it ** is safe to run "fossil backup" on a repository that is in active use. ** ** Only the main repository database is backed up by this command. The ** open check-out file (if any) is not saved. Nor is the global configuration ** database. ** ** Options: ** ** --overwrite OK to overwrite an existing file ** -R NAME Filename of the repository to backup */ void backup_cmd(void){ char *zDest; int bOverwrite = 0; db_find_and_open_repository(OPEN_ANY_SCHEMA, 0); bOverwrite = find_option("overwrite",0,0)!=0; verify_all_options(); if( g.argc!=3 ){ usage("FILE|DIRECTORY"); } zDest = g.argv[2]; if( file_isdir(zDest, ExtFILE)==1 ){ zDest = mprintf("%s/%s", zDest, file_tail(g.zRepositoryName)); } if( file_isfile(zDest, ExtFILE) ){ if( bOverwrite ){ if( file_delete(zDest) ){ fossil_fatal("unable to delete old copy of \"%s\"", zDest); } }else{ fossil_fatal("backup \"%s\" already exists", zDest); } } db_unprotect(PROTECT_ALL); db_multi_exec("VACUUM repository INTO %Q", zDest); }