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