1 /*
2 ** Copyright (c) 2007 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains code used to clone a repository
19 */
20 #include "config.h"
21 #include "clone.h"
22 #include <assert.h>
23 
24 /*
25 ** If there are public BLOBs that deltas from private BLOBs, then
26 ** undeltify the public BLOBs so that the private BLOBs may be safely
27 ** deleted.
28 */
fix_private_blob_dependencies(int showWarning)29 void fix_private_blob_dependencies(int showWarning){
30   Bag toUndelta;
31   Stmt q;
32   int rid;
33 
34   /* Careful:  We are about to delete all BLOB entries that are private.
35   ** So make sure that any no public BLOBs are deltas from a private BLOB.
36   ** Otherwise after the deletion, we won't be able to recreate the public
37   ** BLOBs.
38   */
39   db_prepare(&q,
40     "SELECT "
41     "   rid, (SELECT uuid FROM blob WHERE rid=delta.rid),"
42     "   srcid, (SELECT uuid FROM blob WHERE rid=delta.srcid)"
43     "  FROM delta"
44     " WHERE srcid in private AND rid NOT IN private"
45   );
46   bag_init(&toUndelta);
47   while( db_step(&q)==SQLITE_ROW ){
48     int rid = db_column_int(&q, 0);
49     const char *zId = db_column_text(&q, 1);
50     int srcid = db_column_int(&q, 2);
51     const char *zSrc = db_column_text(&q, 3);
52     if( showWarning ){
53       fossil_warning(
54         "public artifact %S (%d) is a delta from private artifact %S (%d)",
55         zId, rid, zSrc, srcid
56       );
57     }
58     bag_insert(&toUndelta, rid);
59   }
60   db_finalize(&q);
61   while( (rid = bag_first(&toUndelta))>0 ){
62     content_undelta(rid);
63     bag_remove(&toUndelta, rid);
64   }
65   bag_clear(&toUndelta);
66 }
67 
68 /*
69 ** Delete all private content from a repository.
70 */
delete_private_content(void)71 void delete_private_content(void){
72   fix_private_blob_dependencies(1);
73   db_multi_exec(
74     "DELETE FROM blob WHERE rid IN private;"
75     "DELETE FROM delta WHERE rid IN private;"
76     "DELETE FROM private;"
77     "DROP TABLE IF EXISTS modreq;"
78   );
79 }
80 
81 
82 /*
83 ** COMMAND: clone
84 **
85 ** Usage: %fossil clone ?OPTIONS? URI ?FILENAME?
86 **
87 ** Make a clone of a repository specified by URI in the local
88 ** file named FILENAME.  If FILENAME is omitted, then an appropriate
89 ** filename is deduced from last element of the path in the URL.
90 **
91 ** URI may be one of the following forms ([...] denotes optional elements):
92 **
93 **  * HTTP/HTTPS protocol:
94 **
95 **      http[s]://[userid[:password]@]host[:port][/path]
96 **
97 **  * SSH protocol:
98 **
99 **      ssh://[userid@]host[:port]/path/to/repo.fossil[?fossil=path/fossil.exe]
100 **
101 **  * Filesystem:
102 **
103 **      [file://]path/to/repo.fossil
104 **
105 ** For ssh and filesystem, path must have an extra leading
106 ** '/' to use an absolute path.
107 **
108 ** Use %HH escapes for special characters in the userid and
109 ** password.  For example "%40" in place of "@", "%2f" in place
110 ** of "/", and "%3a" in place of ":".
111 **
112 ** Note that in Fossil (in contrast to some other DVCSes) a repository
113 ** is distinct from a checkout.  Cloning a repository is not the same thing
114 ** as opening a repository.  This command always clones the repository.  This
115 ** command might also open the repository, but only if the --no-open option
116 ** is omitted and either the --workdir option is included or the FILENAME
117 ** argument is omitted.  Use the separate [[open]] command to open a
118 ** repository that was previously cloned and already exists on the
119 ** local machine.
120 **
121 ** By default, the current login name is used to create the default
122 ** admin user for the new clone. This can be overridden using
123 ** the -A|--admin-user parameter.
124 **
125 ** Options:
126 **    -A|--admin-user USERNAME   Make USERNAME the administrator
127 **    -B|--httpauth USER:PASS    Add HTTP Basic Authorization to requests
128 **    --nested                   Allow opening a repository inside an opened
129 **                               checkout
130 **    --nocompress               Omit extra delta compression
131 **    --no-open                  Clone only.  Do not open a check-out.
132 **    --once                     Don't remember the URI.
133 **    --private                  Also clone private branches
134 **    --save-http-password       Remember the HTTP password without asking
135 **    --ssh-command|-c SSH       Use SSH as the "ssh" command
136 **    --ssl-identity FILENAME    Use the SSL identity if requested by the server
137 **    -u|--unversioned           Also sync unversioned content
138 **    -v|--verbose               Show more statistics in output
139 **    --workdir DIR              Also open a checkout in DIR
140 **
141 ** See also: [[init]], [[open]]
142 */
clone_cmd(void)143 void clone_cmd(void){
144   char *zPassword;
145   const char *zDefaultUser;   /* Optional name of the default user */
146   const char *zHttpAuth;      /* HTTP Authorization user:pass information */
147   int nErr = 0;
148   int urlFlags = URL_PROMPT_PW | URL_REMEMBER;
149   int syncFlags = SYNC_CLONE;
150   int noCompress = find_option("nocompress",0,0)!=0;
151   int noOpen = find_option("no-open",0,0)!=0;
152   int allowNested = find_option("nested",0,0)!=0; /* Used by open */
153   const char *zRepo = 0;      /* Name of the new local repository file */
154   const char *zWorkDir = 0;   /* Open in this directory, if not zero */
155 
156 
157   /* Also clone private branches */
158   if( find_option("private",0,0)!=0 ) syncFlags |= SYNC_PRIVATE;
159   if( find_option("once",0,0)!=0) urlFlags &= ~URL_REMEMBER;
160   if( find_option("save-http-password",0,0)!=0 ){
161     urlFlags &= ~URL_PROMPT_PW;
162     urlFlags |= URL_REMEMBER_PW;
163   }
164   if( find_option("verbose","v",0)!=0) syncFlags |= SYNC_VERBOSE;
165   if( find_option("unversioned","u",0)!=0 ) syncFlags |= SYNC_UNVERSIONED;
166   zHttpAuth = find_option("httpauth","B",1);
167   zDefaultUser = find_option("admin-user","A",1);
168   zWorkDir = find_option("workdir", 0, 1);
169   clone_ssh_find_options();
170   url_proxy_options();
171 
172   /* We should be done with options.. */
173   verify_all_options();
174 
175   if( g.argc < 3 ){
176     usage("?OPTIONS? FILE-OR-URL ?NEW-REPOSITORY?");
177   }
178   db_open_config(0, 0);
179   if( g.argc==4 ){
180     zRepo = g.argv[3];
181   }else{
182     char *zBase = url_to_repo_basename(g.argv[2]);
183     if( zBase==0 ){
184       fossil_fatal(
185         "unable to guess a repository name from the url \"%s\".\n"
186         "give the repository filename as an additional argument.",
187         g.argv[2]);
188     }
189     zRepo = mprintf("./%s.fossil", zBase);
190     if( zWorkDir==0 ){
191       zWorkDir = mprintf("./%s", zBase);
192     }
193     fossil_free(zBase);
194   }
195   if( -1 != file_size(zRepo, ExtFILE) ){
196     fossil_fatal("file already exists: %s", zRepo);
197   }
198   /* Fail before clone if open will fail because inside an open checkout */
199   if( zWorkDir!=0 && zWorkDir[0]!=0 && !noOpen ){
200     if( db_open_local_v2(0, allowNested) ){
201       fossil_fatal("there is already an open tree at %s", g.zLocalRoot);
202     }
203   }
204   url_parse(g.argv[2], urlFlags);
205   if( zDefaultUser==0 && g.url.user!=0 ) zDefaultUser = g.url.user;
206   if( g.url.isFile ){
207     file_copy(g.url.name, zRepo);
208     db_close(1);
209     db_open_repository(zRepo);
210     db_open_config(1,0);
211     db_record_repository_filename(zRepo);
212     url_remember();
213     if( !(syncFlags & SYNC_PRIVATE) ) delete_private_content();
214     shun_artifacts();
215     db_create_default_users(1, zDefaultUser);
216     if( zDefaultUser ){
217       g.zLogin = zDefaultUser;
218     }else{
219       g.zLogin = db_text(0, "SELECT login FROM user WHERE cap LIKE '%%s%%'");
220     }
221     fossil_print("Repository cloned into %s\n", zRepo);
222   }else{
223     db_close_config();
224     db_create_repository(zRepo);
225     db_open_repository(zRepo);
226     db_open_config(0,0);
227     db_begin_transaction();
228     db_record_repository_filename(zRepo);
229     db_initial_setup(0, 0, zDefaultUser);
230     user_select();
231     db_set("content-schema", CONTENT_SCHEMA, 0);
232     db_set("aux-schema", AUX_SCHEMA_MAX, 0);
233     db_set("rebuilt", get_version(), 0);
234     db_unset("hash-policy", 0);
235     remember_or_get_http_auth(zHttpAuth, urlFlags & URL_REMEMBER, g.argv[2]);
236     url_remember();
237     if( g.zSSLIdentity!=0 ){
238       /* If the --ssl-identity option was specified, store it as a setting */
239       Blob fn;
240       blob_zero(&fn);
241       file_canonical_name(g.zSSLIdentity, &fn, 0);
242       db_unprotect(PROTECT_ALL);
243       db_set("ssl-identity", blob_str(&fn), 0);
244       db_protect_pop();
245       blob_reset(&fn);
246     }
247     db_unprotect(PROTECT_CONFIG);
248     db_multi_exec(
249       "REPLACE INTO config(name,value,mtime)"
250       " VALUES('server-code', lower(hex(randomblob(20))), now());"
251       "DELETE FROM config WHERE name='project-code';"
252     );
253     db_protect_pop();
254     url_enable_proxy(0);
255     clone_ssh_db_set_options();
256     url_get_password_if_needed();
257     g.xlinkClusterOnly = 1;
258     nErr = client_sync(syncFlags,CONFIGSET_ALL,0,0);
259     g.xlinkClusterOnly = 0;
260     verify_cancel();
261     db_end_transaction(0);
262     db_close(1);
263     if( nErr ){
264       file_delete(zRepo);
265       fossil_fatal("server returned an error - clone aborted");
266     }
267     db_open_repository(zRepo);
268   }
269   db_begin_transaction();
270   if( db_exists("SELECT 1 FROM delta WHERE srcId IN phantom") ){
271     fossil_fatal("there are unresolved deltas -"
272                  " the clone is probably incomplete and unusable.");
273   }
274   fossil_print("Rebuilding repository meta-data...\n");
275   rebuild_db(0, 1, 0);
276   if( !noCompress ){
277     fossil_print("Extra delta compression... "); fflush(stdout);
278     extra_deltification();
279     fossil_print("\n");
280   }
281   db_end_transaction(0);
282   fossil_print("Vacuuming the database... "); fflush(stdout);
283   if( db_int(0, "PRAGMA page_count")>1000
284    && db_int(0, "PRAGMA page_size")<8192 ){
285      db_multi_exec("PRAGMA page_size=8192;");
286   }
287   db_unprotect(PROTECT_ALL);
288   db_multi_exec("VACUUM");
289   db_protect_pop();
290   fossil_print("\nproject-id: %s\n", db_get("project-code", 0));
291   fossil_print("server-id:  %s\n", db_get("server-code", 0));
292   zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
293   fossil_print("admin-user: %s (password is \"%s\")\n", g.zLogin, zPassword);
294   if( zWorkDir!=0 && zWorkDir[0]!=0 && !noOpen ){
295     Blob cmd;
296     fossil_print("opening the new %s repository in directory %s...\n",
297        zRepo, zWorkDir);
298     blob_init(&cmd, 0, 0);
299     blob_append_escaped_arg(&cmd, g.nameOfExe, 1);
300     blob_append(&cmd, " open ", -1);
301     blob_append_escaped_arg(&cmd, zRepo, 1);
302     blob_append(&cmd, " --workdir ", -1);
303     blob_append_escaped_arg(&cmd, zWorkDir, 1);
304     if( allowNested ){
305       blob_append(&cmd, " --nested", -1);
306     }
307     fossil_system(blob_str(&cmd));
308     blob_reset(&cmd);
309   }
310 }
311 
312 /*
313 ** If user chooses to use HTTP Authentication over unencrypted HTTP,
314 ** remember decision.  Otherwise, if the URL is being changed and no
315 ** preference has been indicated, err on the safe side and revert the
316 ** decision. Set the global preference if the URL is not being changed.
317 */
remember_or_get_http_auth(const char * zHttpAuth,int fRemember,const char * zUrl)318 void remember_or_get_http_auth(
319   const char *zHttpAuth,  /* Credentials in the form "user:password" */
320   int fRemember,          /* True to remember credentials for later reuse */
321   const char *zUrl        /* URL for which these credentials apply */
322 ){
323   if( zHttpAuth && zHttpAuth[0] ){
324     g.zHttpAuth = mprintf("%s", zHttpAuth);
325   }
326   if( fRemember ){
327     if( g.zHttpAuth && g.zHttpAuth[0] ){
328       set_httpauth(g.zHttpAuth);
329     }else if( zUrl && zUrl[0] ){
330       db_unset_mprintf(0, "http-auth:%s", g.url.canonical);
331     }else{
332       g.zHttpAuth = get_httpauth();
333     }
334   }else if( g.zHttpAuth==0 && zUrl==0 ){
335     g.zHttpAuth = get_httpauth();
336   }
337 }
338 
339 /*
340 ** Get the HTTP Authorization preference from db.
341 */
get_httpauth(void)342 char *get_httpauth(void){
343   char *zKey = mprintf("http-auth:%s", g.url.canonical);
344   char * rc = unobscure(db_get(zKey, 0));
345   free(zKey);
346   return rc;
347 }
348 
349 /*
350 ** Set the HTTP Authorization preference in db.
351 */
set_httpauth(const char * zHttpAuth)352 void set_httpauth(const char *zHttpAuth){
353   db_set_mprintf(obscure(zHttpAuth), 0, "http-auth:%s", g.url.canonical);
354 }
355 
356 /*
357 ** Look for SSH clone command line options and setup in globals.
358 */
clone_ssh_find_options(void)359 void clone_ssh_find_options(void){
360   const char *zSshCmd;        /* SSH command string */
361 
362   zSshCmd = find_option("ssh-command","c",1);
363   if( zSshCmd && zSshCmd[0] ){
364     g.zSshCmd = mprintf("%s", zSshCmd);
365   }
366 }
367 
368 /*
369 ** Set SSH options discovered in global variables (set from command line
370 ** options).
371 */
clone_ssh_db_set_options(void)372 void clone_ssh_db_set_options(void){
373   if( g.zSshCmd && g.zSshCmd[0] ){
374     db_unprotect(PROTECT_ALL);
375     db_set("ssh-command", g.zSshCmd, 0);
376     db_protect_pop();
377   }
378 }
379 
380 /*
381 ** WEBPAGE: download
382 **
383 ** Provide a simple page that enables newbies to download the latest tarball or
384 ** ZIP archive, and provides instructions on how to clone.
385 */
download_page(void)386 void download_page(void){
387   login_check_credentials();
388   style_header("Download Page");
389   if( !g.perm.Zip ){
390     @ <p>Bummer.  You do not have permission to download.
391     if( g.zLogin==0 || g.zLogin[0]==0 ){
392       @ Maybe it would work better if you
393       @ %z(href("%R/login"))logged in</a>.
394     }else{
395       @ Contact the site administrator and ask them to give
396       @ you "Download Zip" privileges.
397     }
398   }else{
399     const char *zDLTag = db_get("download-tag","trunk");
400     const char *zNm = db_get("short-project-name","download");
401     char *zUrl = href("%R/zip/%t/%t.zip", zDLTag, zNm);
402     @ <p>ZIP Archive: %z(zUrl)%h(zNm).zip</a>
403     zUrl = href("%R/tarball/%t/%t.tar.gz", zDLTag, zNm);
404     @ <p>Tarball: %z(zUrl)%h(zNm).tar.gz</a>
405     zUrl = href("%R/sqlar/%t/%t.sqlar", zDLTag, zNm);
406     @ <p>SQLite Archive: %z(zUrl)%h(zNm).sqlar</a>
407   }
408   if( !g.perm.Clone ){
409     @ <p>You are not authorized to clone this repository.
410     if( g.zLogin==0 || g.zLogin[0]==0 ){
411       @ Maybe you would be able to clone if you
412       @ %z(href("%R/login"))logged in</a>.
413     }else{
414       @ Contact the site administrator and ask them to give
415       @ you "Clone" privileges in order to clone.
416     }
417   }else{
418     const char *zNm = db_get("short-project-name","clone");
419     @ <p>Clone the repository using this command:
420     @ <blockquote><pre>
421     @ fossil  clone  %s(g.zBaseURL)  %h(zNm).fossil
422     @ </pre></blockquote>
423   }
424   style_finish_page();
425 }
426