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] [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::branch | options::opts::revision)
322{
323 if (args.size() < 1 || args.size() > 2 || 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 N(app.opts.branch_given && !app.opts.branchname().empty(),
332 F("you must specify a branch to clone"));
333
334 if (args.size() == 1)
335 {
336 // No checkout dir specified, use branch name for dir.
337 workspace_dir = system_path(app.opts.branchname());
338 }
339 else
340 {
341 // Checkout to specified dir.
342 workspace_dir = system_path(idx(args, 1));
343 }
344
345 require_path_is_nonexistent
346 (workspace_dir, F("clone destination directory '%s' already exists") % workspace_dir);
347
348 // remember the initial working dir so that relative file://
349 // db URIs will work
350 system_path start_dir(get_current_working_dir());
351
352 bool internal_db = !app.opts.dbname_given || app.opts.dbname.empty();
353
354 dir_cleanup_helper remove_on_fail(workspace_dir, internal_db);
355
356 // paths.cc's idea of the current workspace root is wrong at this point
357 if (internal_db)
358 app.opts.dbname = system_path(workspace_dir
359 / bookkeeping_root_component
360 / ws_internal_db_file_name);
361
362 // must do this after setting dbname so that _MTN/options is written
363 // correctly
364 workspace::create_workspace(app.opts, app.lua, workspace_dir);
365
366 database db(app);
367 if (get_path_status(db.get_filename()) == path::nonexistent)
368 db.initialize();
369
370 db.ensure_open();
371
372 key_store keys(app);
373 project_t project(db);
374
375 info.client.include_pattern = globish(app.opts.branchname());
376 info.client.exclude_pattern = globish(app.opts.exclude_patterns);
377 build_client_connection_info(app.opts, app.lua, db, keys,
378 info, true, true);
379
380 if (app.opts.signing_key() == "")
381 P(F("doing anonymous pull; use -kKEYNAME if you need authentication"));
382
383 // make sure we're back in the original dir so that file: URIs work
384 change_current_working_dir(start_dir);
385
386 run_netsync_protocol(app.opts, app.lua, project, keys,
387 client_voice, sink_role, info);
388
389 change_current_working_dir(workspace_dir);
390
391 transaction_guard guard(db, false);
392
393 if (app.opts.revision_selectors.size() == 0)
394 {
395 // use branch head revision
396 N(!app.opts.branchname().empty(),
397 F("use --revision or --branch to specify what to checkout"));
398
399 set<revision_id> heads;
400 project.get_branch_heads(app.opts.branchname, heads,
401 app.opts.ignore_suspend_certs);
402 N(heads.size() > 0,
403 F("branch '%s' is empty") % app.opts.branchname);
404 if (heads.size() > 1)
405 {
406 P(F("branch %s has multiple heads:") % app.opts.branchname);
407 for (set<revision_id>::const_iterator i = heads.begin(); i != heads.end(); ++i)
408 P(i18n_format(" %s")
409 % describe_revision(project, *i));
410 P(F("choose one with '%s checkout -r<id>'") % ui.prog_name);
411 E(false, F("branch %s has multiple heads") % app.opts.branchname);
412 }
413 ident = *(heads.begin());
414 }
415 else if (app.opts.revision_selectors.size() == 1)
416 {
417 // use specified revision
418 complete(app.opts, app.lua, project, idx(app.opts.revision_selectors, 0)(), ident);
419
420 guess_branch(app.opts, project, ident);
421 I(!app.opts.branchname().empty());
422
423 N(project.revision_is_in_branch(ident, app.opts.branchname),
424 F("revision %s is not a member of branch %s")
425 % ident % app.opts.branchname);
426 }
427
428 roster_t empty_roster, current_roster;
429
430 L(FL("checking out revision %s to directory %s")
431 % ident % workspace_dir);
432 db.get_roster(ident, current_roster);
433
434 workspace work(app);
435 revision_t workrev;
436 make_revision_for_workspace(ident, cset(), workrev);
437 work.put_work_rev(workrev);
438
439 cset checkout;
440 make_cset(empty_roster, current_roster, checkout);
441
442 content_merge_checkout_adaptor wca(db);
443
444 work.perform_content_update(db, checkout, wca, false);
445
446 work.update_any_attrs(db);
447 work.maybe_update_inodeprints(db);
448 guard.commit();
449 remove_on_fail.commit();
450}
451
452struct pid_file
453{
454 explicit pid_file(system_path const & p)
455 : path(p)
456 {
457 if (path.empty())
458 return;
459 require_path_is_nonexistent(path, F("pid file '%s' already exists") % path);
460 file.open(path.as_external().c_str());
461 E(file.is_open(), F("failed to create pid file '%s'") % path);
462 file << get_process_id() << '\n';
463 file.flush();
464 }
465
466 ~pid_file()
467 {
468 if (path.empty())
469 return;
470 pid_t pid;
471 ifstream(path.as_external().c_str()) >> pid;
472 if (pid == get_process_id()) {
473 file.close();
474 delete_file(path);
475 }
476 }
477
478private:
479 ofstream file;
480 system_path path;
481};
482
483CMD_NO_WORKSPACE(serve, "serve", "", CMD_REF(network), "",
484 N_("Serves the database to connecting clients"),
485 "",
486 options::opts::bind | options::opts::pidfile |
487 options::opts::bind_stdio | options::opts::no_transport_auth )
488{
489 if (!args.empty())
490 throw usage(execid);
491
492 database db(app);
493 key_store keys(app);
494 project_t project(db);
495 pid_file pid(app.opts.pidfile);
496
497 db.ensure_open();
498
499 netsync_connection_info info;
500 info.server.addrs = app.opts.bind_uris;
501
502 if (app.opts.use_transport_auth)
503 {
504 N(app.lua.hook_persist_phrase_ok(),
505 F("need permission to store persistent passphrase "
506 "(see hook persist_phrase_ok())"));
507
508 info.client.include_pattern = globish("*");
509 info.client.exclude_pattern = globish("");
510 if (!app.opts.bind_uris.empty())
511 info.client.unparsed = *app.opts.bind_uris.begin();
512 find_key(app.opts, app.lua, db, keys, info);
513 }
514 else if (!app.opts.bind_stdio)
515 W(F("The --no-transport-auth option is usually only used "
516 "in combination with --stdio"));
517
518 run_netsync_protocol(app.opts, app.lua, project, keys,
519 server_voice, source_and_sink_role, info);
520}
521
522// Local Variables:
523// mode: C++
524// fill-column: 76
525// c-file-style: "gnu"
526// indent-tabs-mode: nil
527// End:
528// 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