From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/sync.c | 928 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 928 insertions(+) create mode 100644 src/sync.c (limited to 'src/sync.c') diff --git a/src/sync.c b/src/sync.c new file mode 100644 index 0000000..860a6dd --- /dev/null +++ b/src/sync.c @@ -0,0 +1,928 @@ +/* +** 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 || g.url.useProxy) && (syncFlags & SYNC_QUIET)==0 ){ + 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); + }else if( syncFlags & SYNC_PING ){ + fossil_print("Ping %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 = 0; /* Number of errors seen */ + int nOther; /* Number of extra remote URLs */ + char **azOther; /* Text of extra remote URLs */ + int i; /* Loop counter */ + int iEnd; /* Loop termination point */ + int nextIEnd; /* Loop termination point for next pass */ + int iPass; /* Which pass through the remotes. 0 or 1 */ + int nPass; /* Number of passes to make. 1 or 2 */ + Stmt q; /* An SQL statement */ + UrlData baseUrl; /* Saved parse of the default remote */ + + sync_explain(syncFlags); + if( (syncFlags & SYNC_ALLURL)==0 ){ + /* Common-case: Only sync with the remote identified by g.url */ + nErr = client_sync(syncFlags, configRcvMask, configSendMask, zAltPCode, 0); + if( nErr==0 ) url_remember(); + return nErr; + } + + /* If we reach this point, it means we want to sync with all remotes */ + memset(&baseUrl, 0, sizeof(baseUrl)); + url_move_parse(&baseUrl, &g.url); + 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); + iEnd = nOther+1; + nextIEnd = 0; + nPass = 1 + ((syncFlags & (SYNC_PUSH|SYNC_PULL))==(SYNC_PUSH|SYNC_PULL)); + for(iPass=0; iPass0 || iPass>0 ) sync_explain(syncFlags); + rc = client_sync(syncFlags, configRcvMask, configSendMask, + zAltPCode, &nRcvd); + if( nRcvd>0 ){ + /* If new artifacts were received, we want to repeat all prior + ** remotes on the second pass */ + nextIEnd = i; + } + nErr += rc; + if( rc==0 && iPass==0 ){ + if( i==0 ){ + url_remember(); + }else if( (g.url.flags & URL_REMEMBER_PW)!=0 ){ + char *zKey = mprintf("sync-pw:%s", azOther[i-1]); + char *zPw = obscure(g.url.passwd); + if( zPw && zPw[0] ){ + db_set(zKey/*works-like:""*/, zPw, 0); + } + fossil_free(zPw); + fossil_free(zKey); + } + } + if( i==0 ){ + url_move_parse(&baseUrl, &g.url); /* Don't forget canonical URL */ + }else{ + url_unparse(&g.url); /* Delete auxiliary URL parses */ + } + } + iEnd = nextIEnd; + } + 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; + int showPw; + db_find_and_open_repository(0, 0); + showPw = find_option("show-passwords",0,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-passwords]" + ** + ** Show the CONFIG table entries that relate to remembering remote URLs + */ + Stmt q; + int n; + sqlite3_create_function(g.db, "unobscure", 1, SQLITE_UTF8, &g.db, + db_obscure, 0, 0); + 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 NOT LIKE '%%sync-pw%%' AND name<>'parent-project-pw'" + " THEN value" + " WHEN %d THEN unobscure(value)" + " ELSE printf('%%.*c',length(value)/2-1,'*') 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", + showPw + ); + 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); +} -- cgit v1.3.1