monotone

monotone Mtn Source Tree

Root/cmd_netsync.cc

1#include "base.hh"
2#include "cmd.hh"
3
4#include "diff_patch.hh"
5#include "netcmd.hh"
6#include "globish.hh"
7#include "keys.hh"
8#include "key_store.hh"
9#include "cert.hh"
10#include "revision.hh"
11#include "ui.hh"
12#include "uri.hh"
13#include "vocab_cast.hh"
14#include "platform-wrapped.hh"
15#include "app_state.hh"
16#include "project.hh"
17#include "work.hh"
18#include "database.hh"
19#include "roster.hh"
20
21#include <fstream>
22
23using std::ifstream;
24using std::ofstream;
25using std::map;
26using std::set;
27using std::string;
28using std::vector;
29
30using boost::shared_ptr;
31
32static const var_key default_server_key(var_domain("database"),
33 var_name("default-server"));
34static const var_key default_include_pattern_key(var_domain("database"),
35 var_name("default-include-pattern"));
36static const var_key default_exclude_pattern_key(var_domain("database"),
37 var_name("default-exclude-pattern"));
38
39static char const ws_internal_db_file_name[] = "mtn.db";
40
41static void
42find_key(options & opts,
43 lua_hooks & lua,
44 database & db,
45 key_store & keys,
46 netsync_connection_info const & info,
47 bool need_key = true)
48{
49 if (!opts.signing_key().empty())
50 return;
51
52 rsa_keypair_id key;
53
54 utf8 host(info.client.unparsed);
55 if (!info.client.u.host.empty())
56 host = utf8(info.client.u.host);
57
58 if (!lua.hook_get_netsync_key(host,
59 info.client.include_pattern,
60 info.client.exclude_pattern, key)
61 && need_key)
62 get_user_key(opts, lua, db, keys, key);
63
64 opts.signing_key = key;
65}
66
67static void
68build_client_connection_info(options & opts,
69 lua_hooks & lua,
70 database & db,
71 key_store & keys,
72 netsync_connection_info & info,
73 bool address_given,
74 bool include_or_exclude_given,
75 bool need_key = true)
76{
77 // Use the default values if needed and available.
78 if (!address_given)
79 {
80 N(db.var_exists(default_server_key),
81 F("no server given and no default server set"));
82 var_value addr_value;
83 db.get_var(default_server_key, addr_value);
84 info.client.unparsed = utf8(addr_value());
85 L(FL("using default server address: %s") % info.client.unparsed);
86 }
87 parse_uri(info.client.unparsed(), info.client.u);
88 if (info.client.u.query.empty() && !include_or_exclude_given)
89 {
90 // No include/exclude given anywhere, use the defaults.
91 N(db.var_exists(default_include_pattern_key),
92 F("no branch pattern given and no default pattern set"));
93 var_value pattern_value;
94 db.get_var(default_include_pattern_key, pattern_value);
95 info.client.include_pattern = globish(pattern_value());
96 L(FL("using default branch include pattern: '%s'")
97 % info.client.include_pattern);
98 if (db.var_exists(default_exclude_pattern_key))
99 {
100 db.get_var(default_exclude_pattern_key, pattern_value);
101 info.client.exclude_pattern = globish(pattern_value());
102 }
103 else
104 info.client.exclude_pattern = globish();
105 L(FL("excluding: %s") % info.client.exclude_pattern);
106 }
107 else if(!info.client.u.query.empty())
108 {
109 N(!include_or_exclude_given,
110 F("Include/exclude pattern was given both as part of the URL and as a separate argument."));
111
112 // Pull include/exclude from the query string
113 char const separator = '/';
114 char const negate = '-';
115 string const & query(info.client.u.query);
116 std::vector<arg_type> includes, excludes;
117 string::size_type begin = 0;
118 string::size_type end = query.find(separator);
119 while (begin < query.size())
120 {
121 std::string item = query.substr(begin, end);
122 if (end == string::npos)
123 begin = end;
124 else
125 {
126 begin = end+1;
127 if (begin < query.size())
128 end = query.find(separator, begin);
129 }
130
131 bool is_exclude = false;
132 if (item.size() >= 1 && item.at(0) == negate)
133 {
134 is_exclude = true;
135 item.erase(0, 1);
136 }
137 else if (item.find("include=") == 0)
138 {
139 item.erase(0, string("include=").size());
140 }
141 else if (item.find("exclude=") == 0)
142 {
143 is_exclude = true;
144 item.erase(0, string("exclude=").size());
145 }
146
147 if (is_exclude)
148 excludes.push_back(arg_type(urldecode(item)));
149 else
150 includes.push_back(arg_type(urldecode(item)));
151 }
152 info.client.include_pattern = globish(includes);
153 info.client.exclude_pattern = globish(excludes);
154 }
155
156 // Maybe set the default values.
157 if (!db.var_exists(default_server_key) || opts.set_default)
158 {
159 P(F("setting default server to %s") % info.client.unparsed());
160 db.set_var(default_server_key, var_value(info.client.unparsed()));
161 }
162 if (!db.var_exists(default_include_pattern_key)
163 || opts.set_default)
164 {
165 P(F("setting default branch include pattern to '%s'")
166 % info.client.include_pattern);
167 db.set_var(default_include_pattern_key,
168 var_value(info.client.include_pattern()));
169 }
170 if (!db.var_exists(default_exclude_pattern_key)
171 || opts.set_default)
172 {
173 P(F("setting default branch exclude pattern to '%s'")
174 % info.client.exclude_pattern);
175 db.set_var(default_exclude_pattern_key,
176 var_value(info.client.exclude_pattern()));
177 }
178
179 info.client.use_argv =
180 lua.hook_get_netsync_connect_command(info.client.u,
181 info.client.include_pattern,
182 info.client.exclude_pattern,
183 global_sanity.debug_p(),
184 info.client.argv);
185 opts.use_transport_auth = lua.hook_use_transport_auth(info.client.u);
186 if (opts.use_transport_auth)
187 {
188 find_key(opts, lua, db, keys, info, need_key);
189 }
190}
191
192static void
193extract_client_connection_info(options & opts,
194 lua_hooks & lua,
195 database & db,
196 key_store & keys,
197 args_vector const & args,
198 netsync_connection_info & info,
199 bool need_key = true)
200{
201 bool have_address = false;
202 bool have_include_exclude = false;
203 if (args.size() >= 1)
204 {
205 have_address = true;
206 info.client.unparsed = idx(args, 0);
207 }
208 if (args.size() >= 2 || opts.exclude_given)
209 {
210 E(args.size() >= 2, F("no branch pattern given"));
211
212 have_include_exclude = true;
213 info.client.include_pattern = globish(args.begin() + 1, args.end());
214 info.client.exclude_pattern = globish(opts.exclude_patterns);
215 }
216 build_client_connection_info(opts, lua, db, keys,
217 info, have_address, have_include_exclude,
218 need_key);
219}
220
221CMD(push, "push", "", CMD_REF(network),
222 N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
223 N_("Pushes branches to a netsync server"),
224 N_("This will push all branches that match the pattern given in PATTERN "
225 "to the netsync server at the address ADDRESS."),
226 options::opts::set_default | options::opts::exclude |
227 options::opts::key_to_push)
228{
229 database db(app);
230 key_store keys(app);
231 project_t project(db);
232
233 netsync_connection_info info;
234 extract_client_connection_info(app.opts, app.lua, db, keys, args, info);
235
236 run_netsync_protocol(app.opts, app.lua, project, keys,
237 client_voice, source_role, info);
238}
239
240CMD(pull, "pull", "", CMD_REF(network),
241 N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
242 N_("Pulls branches from a netsync server"),
243 N_("This pulls all branches that match the pattern given in PATTERN "
244 "from the netsync server at the address ADDRESS."),
245 options::opts::set_default | options::opts::exclude)
246{
247 database db(app);
248 key_store keys(app);
249 project_t project(db);
250
251 netsync_connection_info info;
252 extract_client_connection_info(app.opts, app.lua, db, keys,
253 args, info, false);
254
255 if (app.opts.signing_key() == "")
256 P(F("doing anonymous pull; use -kKEYNAME if you need authentication"));
257
258 run_netsync_protocol(app.opts, app.lua, project, keys,
259 client_voice, sink_role, info);
260}
261
262CMD(sync, "sync", "", CMD_REF(network),
263 N_("[ADDRESS[:PORTNUMBER] [PATTERN ...]]"),
264 N_("Synchronizes branches with a netsync server"),
265 N_("This synchronizes branches that match the pattern given in PATTERN "
266 "with the netsync server at the address ADDRESS."),
267 options::opts::set_default | options::opts::exclude |
268 options::opts::key_to_push)
269{
270 database db(app);
271 key_store keys(app);
272 project_t project(db);
273
274 netsync_connection_info info;
275 extract_client_connection_info(app.opts, app.lua, db, keys, args, info);
276
277 if (app.opts.set_default && workspace::found)
278 {
279 // Write workspace options, including key; this is the simplest way to
280 // fix a "found multiple keys" error reported by sync.
281 workspace work(app, true);
282 }
283
284 run_netsync_protocol(app.opts, app.lua, project, keys,
285 client_voice, source_and_sink_role, info);
286}
287
288class dir_cleanup_helper
289{
290public:
291 dir_cleanup_helper(system_path const & new_dir, bool i_db) :
292 commited(false), internal_db(i_db), dir(new_dir) {}
293 ~dir_cleanup_helper()
294 {
295 if (!commited && directory_exists(dir))
296 {
297#ifdef WIN32
298 if (!internal_db)
299 delete_dir_recursive(dir);
300#else
301 delete_dir_recursive(dir);
302#endif /* WIN32 */
303 }
304 }
305 void commit(void)
306 {
307 commited = true;
308 }
309private:
310 bool commited;
311 bool internal_db;
312 system_path dir;
313};
314
315CMD(clone, "clone", "", CMD_REF(network),
316 N_("ADDRESS[:PORTNUMBER] BRANCH [DIRECTORY]"),
317 N_("Checks out a revision from a remote database into a directory"),
318 N_("If a revision is given, that's the one that will be checked out. "
319 "Otherwise, it will be the head of the branch supplied. "
320 "If no directory is given, the branch name will be used as directory"),
321 options::opts::revision)
322{
323 if (args.size() < 2 || args.size() > 3 || app.opts.revision_selectors.size() > 1)
324 throw usage(execid);
325
326 revision_id ident;
327 system_path workspace_dir;
328 netsync_connection_info info;
329 info.client.unparsed = idx(args, 0);
330
331 branch_name branchname(idx(args, 1)());
332
333 N(!branchname().empty(), F("you must specify a branch to clone"));
334
335 if (args.size() == 2)
336 {
337 // No checkout dir specified, use branch name for dir.
338 workspace_dir = system_path(branchname());
339 }
340 else
341 {
342 // Checkout to specified dir.
343 workspace_dir = system_path(idx(args, 2));
344 }
345
346 require_path_is_nonexistent
347 (workspace_dir, F("clone destination directory '%s' already exists") % workspace_dir);
348
349 // remember the initial working dir so that relative file://
350 // db URIs will work
351 system_path start_dir(get_current_working_dir());
352
353 bool internal_db = !app.opts.dbname_given || app.opts.dbname.empty();
354
355 dir_cleanup_helper remove_on_fail(workspace_dir, internal_db);
356
357 // paths.cc's idea of the current workspace root is wrong at this point
358 if (internal_db)
359 app.opts.dbname = system_path(workspace_dir
360 / bookkeeping_root_component
361 / ws_internal_db_file_name);
362
363 // this is actually stupid, but app.opts.branchname must be set here
364 // otherwise it will not be written into _MTN/options, in case
365 // a revision is chosen which has multiple branch certs
366 app.opts.branchname = branchname;
367 workspace::create_workspace(app.opts, app.lua, workspace_dir);
368 app.opts.branchname = branch_name();
369
370 database db(app);
371 if (get_path_status(db.get_filename()) == path::nonexistent)
372 db.initialize();
373
374 db.ensure_open();
375
376 key_store keys(app);
377 project_t project(db);
378
379 info.client.include_pattern = globish(branchname());
380 info.client.exclude_pattern = globish(app.opts.exclude_patterns);
381
382 build_client_connection_info(app.opts, app.lua, db, keys,
383 info, true, true, false);
384
385 if (app.opts.signing_key() == "")
386 P(F("doing anonymous pull; use -kKEYNAME if you need authentication"));
387
388 // make sure we're back in the original dir so that file: URIs work
389 change_current_working_dir(start_dir);
390
391 run_netsync_protocol(app.opts, app.lua, project, keys,
392 client_voice, sink_role, info);
393
394 change_current_working_dir(workspace_dir);
395
396 transaction_guard guard(db, false);
397
398 if (app.opts.revision_selectors.empty())
399 {
400 set<revision_id> heads;
401 project.get_branch_heads(branchname, heads,
402 app.opts.ignore_suspend_certs);
403 N(!heads.empty(),
404 F("branch '%s' is empty") % branchname);
405 if (heads.size() > 1)
406 {
407 P(F("branch %s has multiple heads:") % branchname);
408 for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
409 P(i18n_format(" %s")
410 % describe_revision(project, *i));
411 P(F("choose one with '%s clone -r<id> SERVER BRANCH'") % ui.prog_name);
412 E(false, F("branch %s has multiple heads") % branchname);
413 }
414 ident = *(heads.begin());
415 }
416 else if (app.opts.revision_selectors.size() == 1)
417 {
418 // use specified revision
419 complete(app.opts, app.lua, project, idx(app.opts.revision_selectors, 0)(), ident);
420
421 N(project.revision_is_in_branch(ident, branchname),
422 F("revision %s is not a member of branch %s")
423 % ident % branchname);
424 }
425
426 roster_t empty_roster, current_roster;
427
428 L(FL("checking out revision %s to directory %s")
429 % ident % workspace_dir);
430 db.get_roster(ident, current_roster);
431
432 workspace work(app);
433 revision_t workrev;
434 make_revision_for_workspace(ident, cset(), workrev);
435 work.put_work_rev(workrev);
436
437 cset checkout;
438 make_cset(empty_roster, current_roster, checkout);
439
440 content_merge_checkout_adaptor wca(db);
441
442 work.perform_content_update(db, checkout, wca, false);
443
444 work.update_any_attrs(db);
445 work.maybe_update_inodeprints(db);
446 guard.commit();
447 remove_on_fail.commit();
448}
449
450struct pid_file
451{
452 explicit pid_file(system_path const & p)
453 : path(p)
454 {
455 if (path.empty())
456 return;
457 require_path_is_nonexistent(path, F("pid file '%s' already exists") % path);
458 file.open(path.as_external().c_str());
459 E(file.is_open(), F("failed to create pid file '%s'") % path);
460 file << get_process_id() << '\n';
461 file.flush();
462 }
463
464 ~pid_file()
465 {
466 if (path.empty())
467 return;
468 pid_t pid;
469 ifstream(path.as_external().c_str()) >> pid;
470 if (pid == get_process_id()) {
471 file.close();
472 delete_file(path);
473 }
474 }
475
476private:
477 ofstream file;
478 system_path path;
479};
480
481CMD_NO_WORKSPACE(serve, "serve", "", CMD_REF(network), "",
482 N_("Serves the database to connecting clients"),
483 "",
484 options::opts::bind | options::opts::pidfile |
485 options::opts::bind_stdio | options::opts::no_transport_auth )
486{
487 if (!args.empty())
488 throw usage(execid);
489
490 database db(app);
491 key_store keys(app);
492 project_t project(db);
493 pid_file pid(app.opts.pidfile);
494
495 db.ensure_open();
496
497 netsync_connection_info info;
498 info.server.addrs = app.opts.bind_uris;
499
500 if (app.opts.use_transport_auth)
501 {
502 N(app.lua.hook_persist_phrase_ok(),
503 F("need permission to store persistent passphrase "
504 "(see hook persist_phrase_ok())"));
505
506 info.client.include_pattern = globish("*");
507 info.client.exclude_pattern = globish("");
508 if (!app.opts.bind_uris.empty())
509 info.client.unparsed = *app.opts.bind_uris.begin();
510 find_key(app.opts, app.lua, db, keys, info);
511 }
512 else if (!app.opts.bind_stdio)
513 W(F("The --no-transport-auth option is usually only used "
514 "in combination with --stdio"));
515
516 run_netsync_protocol(app.opts, app.lua, project, keys,
517 server_voice, source_and_sink_role, info);
518}
519
520// Local Variables:
521// mode: C++
522// fill-column: 76
523// c-file-style: "gnu"
524// indent-tabs-mode: nil
525// End:
526// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s:

Archive Download this file

Branches

Tags

Quick Links:     www.monotone.ca    -     Downloads    -     Documentation    -     Wiki    -     Code Forge    -     Build Status