/*
** 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 <assert.h>
/*
** If the repository is configured for autosyncing, then do an
** autosync. Bits of the "flags" parameter determine details of behavior:
**
** SYNC_PULL Pull content from the server to the local repo
** SYNC_PUSH Push content from local up to the server
** SYNC_CKIN_LOCK Take a check-in lock on the current checkout.
** SYNC_VERBOSE Extra output
**
** Return the number of errors.
**
** The autosync setting can be a boolean or "pullonly". No autosync
** is attempted if the autosync setting is off, and only auto-pull is
** attempted if autosync is set to "pullonly". The check-in lock is
** not acquired unless autosync is set to "on".
**
** If dont-push setting is true, that is the same as having autosync
** set to pullonly.
*/
int autosync(int flags){
const char *zAutosync;
int rc;
int configSync = 0; /* configuration changes transferred */
if( g.fNoSync ){
return 0;
}
zAutosync = db_get("autosync", 0);
if( zAutosync==0 ) zAutosync = "on"; /* defend against misconfig */
if( is_false(zAutosync) ) return 0;
if( db_get_boolean("dont-push",0) || fossil_strncmp(zAutosync,"pull",4)==0 ){
flags &= ~SYNC_CKIN_LOCK;
if( flags & SYNC_PUSH ) return 0;
}
url_parse(0, URL_REMEMBER);
if( g.url.protocol==0 ) return 0;
if( g.url.user!=0 && g.url.passwd==0 ){
g.url.passwd = unobscure(db_get("last-sync-pw", 0));
g.url.flags |= URL_PROMPT_PW;
url_prompt_for_password();
}
g.zHttpAuth = get_httpauth();
url_remember();
if( find_option("verbose","v",0)!=0 ) flags |= SYNC_VERBOSE;
fossil_print("Autosync: %s\n", g.url.canonical);
url_enable_proxy("via proxy: ");
rc = client_sync(flags, configSync, 0, 0);
return rc;
}
/*
** This routine will try a number of times to perform autosync with a
** 0.5 second sleep between attempts.
**
** Return zero on success and non-zero on a failure. If failure occurs
** and doPrompt flag is true, ask the user if they want to continue, and
** if they answer "yes" then return zero in spite of the failure.
*/
int autosync_loop(int flags, int nTries, int doPrompt){
int n = 0;
int rc = 0;
if( (flags & (SYNC_PUSH|SYNC_PULL))==(SYNC_PUSH|SYNC_PULL)
&& db_get_boolean("uv-sync",0)
){
flags |= SYNC_UNVERSIONED;
}
while( (n==0 || n<nTries) && (rc=autosync(flags)) ){
if( rc ){
if( ++n<nTries ){
fossil_warning("Autosync failed, making another attempt.");
sqlite3_sleep(500);
}else{
fossil_warning("Autosync failed.");
}
}
}
if( rc && doPrompt ){
Blob ans;
char cReply;
prompt_user("continue in spite of sync failure (y/N)? ", &ans);
cReply = blob_str(&ans)[0];
if( cReply=='y' || cReply=='Y' ) rc = 0;
blob_reset(&ans);
}
return rc;
}
/*
** This routine processes the command-line argument for push, pull,
** and sync. If a command-line argument is given, that is the URL
** of a server to sync against. If no argument is given, use the
** most recently synced URL. Remember the current URL for next time.
*/
static void process_sync_args(
unsigned *pConfigFlags, /* Write configuration flags here */
unsigned *pSyncFlags, /* Write sync flags here */
int uvOnly, /* Special handling flags for UV sync */
unsigned urlOmitFlags /* Omit these URL flags */
){
const char *zUrl = 0;
const char *zHttpAuth = 0;
unsigned configSync = 0;
unsigned urlFlags = URL_REMEMBER | URL_PROMPT_PW;
int urlOptional = 0;
if( find_option("autourl",0,0)!=0 ){
urlOptional = 1;
urlFlags = 0;
}
zHttpAuth = find_option("httpauth","B",1);
if( find_option("once",0,0)!=0 ) urlFlags &= ~URL_REMEMBER;
if( (*pSyncFlags) & SYNC_FROMPARENT ) urlFlags &= ~URL_REMEMBER;
if( !uvOnly ){
if( find_option("private",0,0)!=0 ){
*pSyncFlags |= SYNC_PRIVATE;
}
/* The --verily option to sync, push, and pull forces extra igot cards
** to be exchanged. This can overcome malfunctions in the sync protocol.
*/
if( find_option("verily",0,0)!=0 ){
*pSyncFlags |= SYNC_RESYNC;
}
}
if( find_option("private",0,0)!=0 ){
*pSyncFlags |= SYNC_PRIVATE;
}
if( find_option("verbose","v",0)!=0 ){
*pSyncFlags |= SYNC_VERBOSE;
}
url_proxy_options();
clone_ssh_find_options();
if( !uvOnly ) db_find_and_open_repository(0, 0);
db_open_config(0, 1);
if( g.argc==2 ){
if( db_get_boolean("auto-shun",0) ) configSync = CONFIGSET_SHUN;
}else if( g.argc==3 ){
zUrl = g.argv[2];
}
if( ((*pSyncFlags) & (SYNC_PUSH|SYNC_PULL))==(SYNC_PUSH|SYNC_PULL)
&& db_get_boolean("uv-sync",0)
){
*pSyncFlags |= SYNC_UNVERSIONED;
}
urlFlags &= ~urlOmitFlags;
if( urlFlags & URL_REMEMBER ){
clone_ssh_db_set_options();
}
url_parse(zUrl, urlFlags);
remember_or_get_http_auth(zHttpAuth, urlFlags & URL_REMEMBER, zUrl);
url_remember();
if( g.url.protocol==0 ){
if( urlOptional ) fossil_exit(0);
usage("URL");
}
user_select();
if( g.url.isAlias ){
if( ((*pSyncFlags) & (SYNC_PUSH|SYNC_PULL))==(SYNC_PUSH|SYNC_PULL) ){
fossil_print("Sync with %s\n", g.url.canonical);
}else if( (*pSyncFlags) & SYNC_PUSH ){
fossil_print("Push to %s\n", g.url.canonical);
}else if( (*pSyncFlags) & SYNC_PULL ){
fossil_print("Pull from %s\n", g.url.canonical);
}
}
url_enable_proxy("via proxy: ");
*pConfigFlags |= configSync;
}
/*
** COMMAND: pull
**
** Usage: %fossil pull ?URL? ?options?
**
** Pull all sharable changes from a remote repository into the local
** repository. Sharable changes include public check-ins, edits to
** wiki pages, tickets, and tech-notes, as well as forum content. Add
** the --private option to pull private branches. Use the
** "configuration pull" command to pull website configuration details.
**
** If URL is not specified, then the URL from the most recent clone, push,
** pull, remote, or sync command is used. See "fossil help clone" for
** details on the URL formats.
**
** Options:
**
** -B|--httpauth USER:PASS Credentials for the simple HTTP auth protocol,
** if required by the remote website
** --from-parent-project Pull content from the parent project
** --ipv4 Use only IPv4, not IPv6
** --once Do not remember URL for subsequent syncs
** --private Pull private branches too
** --project-code CODE Use CODE as the project code
** --proxy PROXY Use the specified HTTP proxy
** -R|--repository REPO Local repository to pull into
** --ssl-identity FILE Local SSL credentials, if requested by remote
** --ssh-command SSH Use SSH as the "ssh" command
** -v|--verbose Additional (debugging) output
** --verily Exchange extra information with the remote
** to ensure no content is overlooked
**
** See also: [[clone]], [[config]], [[push]], [[remote]], [[sync]]
*/
void pull_cmd(void){
unsigned configFlags = 0;
unsigned syncFlags = SYNC_PULL;
unsigned urlOmitFlags = 0;
const char *zAltPCode = find_option("project-code",0,1);
if( find_option("from-parent-project",0,0)!=0 ){
syncFlags |= SYNC_FROMPARENT;
}
if( zAltPCode ) urlOmitFlags = URL_REMEMBER;
process_sync_args(&configFlags, &syncFlags, 0, urlOmitFlags);
/* We should be done with options.. */
verify_all_options();
client_sync(syncFlags, configFlags, 0, zAltPCode);
}
/*
** COMMAND: push
**
** Usage: %fossil push ?URL? ?options?
**
** Push all sharable changes from the local repository to a remote
** repository. Sharable changes include public check-ins, edits to
** wiki pages, tickets, and tech-notes, as well as forum content. Use
** --private to also push private branches. Use the "configuration
** push" command to push website configuration details.
**
** If URL is not specified, then the URL from the most recent clone, push,
** pull, remote, or sync command is used. See "fossil help clone" for
** details on the URL formats.
**
** Options:
**
** -B|--httpauth USER:PASS Credentials for the simple HTTP auth protocol,
** if required by the remote website
** --ipv4 Use only IPv4, not IPv6
** --once Do not remember URL for subsequent syncs
** --proxy PROXY Use the specified HTTP proxy
** --private Push private branches too
** -R|--repository REPO Local repository to push from
** --ssl-identity FILE Local SSL credentials, if requested by remote
** --ssh-command SSH Use SSH as the "ssh" command
** -v|--verbose Additional (debugging) output
** --verily Exchange extra information with the remote
** to ensure no content is overlooked
**
** See also: [[clone]], [[config]], [[pull]], [[remote]], [[sync]]
*/
void push_cmd(void){
unsigned configFlags = 0;
unsigned syncFlags = SYNC_PUSH;
process_sync_args(&configFlags, &syncFlags, 0, 0);
/* We should be done with options.. */
verify_all_options();
if( db_get_boolean("dont-push",0) ){
fossil_fatal("pushing is prohibited: the 'dont-push' option is set");
}
client_sync(syncFlags, 0, 0, 0);
}
/*
** COMMAND: sync
**
** Usage: %fossil sync ?URL? ?options?
**
** Synchronize all sharable changes between the local repository and a
** remote repository. Sharable changes include public check-ins and
** edits to wiki pages, tickets, and technical notes.
**
** If URL is not specified, then the URL from the most recent clone, push,
** pull, remote, or sync command is used. See "fossil help clone" for
** details on the URL formats.
**
** Options:
**
** -B|--httpauth USER:PASS Credentials for the simple HTTP auth protocol,
** if required by the remote website
** --ipv4 Use only IPv4, not IPv6
** --once Do not remember URL for subsequent syncs
** --proxy PROXY Use the specified HTTP proxy
** --private Sync private branches too
** -R|--repository REPO Local repository to sync with
** --ssl-identity FILE Local SSL credentials, if requested by remote
** --ssh-command SSH Use SSH as the "ssh" command
** -u|--unversioned Also sync unversioned content
** -v|--verbose Additional (debugging) output
** --verily Exchange extra information with the remote
** to ensure no content is overlooked
**
** See also: [[clone]], [[pull]], [[push]], [[remote]]
*/
void sync_cmd(void){
unsigned configFlags = 0;
unsigned syncFlags = SYNC_PUSH|SYNC_PULL;
if( find_option("unversioned","u",0)!=0 ){
syncFlags |= SYNC_UNVERSIONED;
}
process_sync_args(&configFlags, &syncFlags, 0, 0);
/* We should be done with options.. */
verify_all_options();
if( db_get_boolean("dont-push",0) ) syncFlags &= ~SYNC_PUSH;
client_sync(syncFlags, configFlags, 0, 0);
if( (syncFlags & SYNC_PUSH)==0 ){
fossil_warning("pull only: the 'dont-push' option is set");
}
}
/*
** Handle the "fossil unversioned sync" and "fossil unversioned revert"
** commands.
*/
void sync_unversioned(unsigned syncFlags){
unsigned configFlags = 0;
(void)find_option("uv-noop",0,0);
process_sync_args(&configFlags, &syncFlags, 1, 0);
verify_all_options();
client_sync(syncFlags, 0, 0, 0);
}
/*
** COMMAND: remote
** COMMAND: remote-url*
**
** Usage: %fossil remote ?SUBCOMMAND ...?
**
** Use this command to view or modify the set of remote repositories
** used as the default target for sync, push, and pull and for autosync.
**
** The default remote is set automatically by a "clone" command or by any
** "sync", "push", or "pull" command that specifies an explicit URL
** and omits the --once flag. The default remote is used by
** auto-syncing and by "sync", "push", and "pull" that omit the server URL.
** Additional remotes can be added using the "add" command or deleted
** using the "delete" command. The name of any additional remote can be
** used as an argument to the "sync", "push", and "pull" commands where
** one would normally put a URL argument.
**
** See "fossil help clone" for further information about URL formats.
**
** The official name of this command is "remote-url" but most people
** use the shortened name "remote".
**
** > fossil remote
**
** With no arguments, this command shows the current default remote.
** Or if there is no default, it shows "off". The default remote is
** used by autosync. The default remote is whatever URL was specified
** for the most recent "sync", "push", or "pull" command that omitted
** the --once option.
**
** > fossil remote add NAME URL
**
** Add a new URL to the set of remotes. The new URL is assigned the
** symbolic identifier "NAME". Subsequently, NAME can be used in place
** of the full URL on commands like "push" and "pull".
**
** > fossil remote delete NAME
**
** Delete a URL previously added by the "add" subcommand.
**
** > fossil remote list
**
** Show all remote URLs
**
** > fossil remote off
**
** Disable the default URL. Use this as a shorthand to prevent
** autosync while in airplane mode, for example.
**
** > fossil remote URL
**
** Make URL the new default URL. The prior default URL is replaced.
*/
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();
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_multi_exec(
"DELETE FROM config WHERE name GLOB 'last-sync-*';"
);
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 ) goto remote_add_default;
url_parse_local(zUrl, URL_PROMPT_PW, &x);
db_begin_write();
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_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_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_commit_transaction();
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)
){
remote_add_default:
db_unset("last-sync-url", 0);
db_unset("last-sync-pw", 0);
url_parse(g.argv[2], URL_REMEMBER|URL_PROMPT_PW|URL_ASK_REMEMBER_PW);
url_remember();
return;
}
fossil_fatal("unknown command \"%s\" - should be a URL or one of: "
"add delete list off", 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 chnages 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 checkout 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_multi_exec("VACUUM repository INTO %Q", zDest);
}