• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MerginMaps / geodiff / 28230499308

26 Jun 2026 09:47AM UTC coverage: 87.554% (-0.6%) from 88.114%
28230499308

Pull #254

github

JanCaha
bump version
Pull Request #254: Add tables to include

74 of 106 new or added lines in 3 files covered. (69.81%)

44 existing lines in 1 file now uncovered.

3679 of 4202 relevant lines covered (87.55%)

574.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.55
/geodiff/src/geodiff-cli.cpp
1
/*
2
 GEODIFF - MIT License
3
 Copyright (C) 2019 Peter Petrik
4
*/
5

6
#include "geodiff.h"
7
#include <iostream>
8
#include <stdio.h>
9
#include <string.h>
10
#include <string>
11

12
#include <fstream>
13
#include <vector>
14
#include <sstream>
15
#include "geodiffutils.hpp"
16
#include "geodiffcontext.hpp"
17
#include "driver.h"
18

19
static bool fileToStdout( const std::string &filepath )
12✔
20
{
21
#ifdef WIN32
22
  std::ifstream f;
23
  try
24
  {
25
    f.open( stringToWString( filepath ) );
26
  }
27
  catch ( GeoDiffException & )
28
  {
29
    return false;
30
  }
31
#else
32
  std::ifstream f( filepath );
12✔
33
#endif
34
  if ( !f.is_open() )
12✔
35
    return false;
×
36

37
  std::cout << f.rdbuf();
12✔
38
  return true;
12✔
39
}
12✔
40

41
static bool parseRequiredArgument( std::string &value, const std::vector<std::string> &args, size_t &i, const std::string &argName, const std::string &cmdName )
149✔
42
{
43
  if ( i < args.size() )
149✔
44
  {
45
    value = args[i++];
123✔
46
    return true;
123✔
47
  }
48
  else
49
  {
50
    std::cout << "Error: missing " << argName << " for '" << cmdName << "' command." << std::endl;
26✔
51
    return false;
26✔
52
  }
53
}
54

55
static bool checkNoExtraArguments( const std::vector<std::string> &args, size_t i, const std::string &cmdName )
42✔
56
{
57
  if ( i < args.size() )
42✔
58
  {
59
    std::cout << "Error: unexpected extra arguments for '" << cmdName << "' command." << std::endl;
12✔
60
    return false;
12✔
61
  }
62
  return true;
30✔
63
}
64

65
static bool isOption( const std::string &str )
86✔
66
{
67
  return str.size() > 0 && str[0] == '-';
86✔
68
}
69

70
static bool parseDriverOption( const std::vector<std::string> &args, size_t &i, const std::string &cmdName, std::string &driverName, std::string &driverOptions, std::string &tablesToSkip, std::string &tablesToInclude )
43✔
71
{
72
  for ( ; i < args.size(); ++i )
48✔
73
  {
74
    if ( !isOption( args[i] ) )
42✔
75
      break;  // no more options
27✔
76

77
    if ( args[i] == "--driver" )
15✔
78
    {
79
      if ( i + 2 >= args.size() )
10✔
80
      {
81
        std::cout << "Error: missing arguments for driver option" << std::endl;
7✔
82
        return false;
7✔
83
      }
84
      driverName = args[i + 1];
3✔
85
      driverOptions = args[i + 2];
3✔
86
      i += 2;
3✔
87
      continue;
3✔
88
    }
89
    else if ( args[i] == "--skip-tables" )
5✔
90
    {
91
      if ( i + 1 >= args.size() )
3✔
92
      {
93
        std::cout << "Error: missing arguments for skip-tables option" << std::endl;
1✔
94
        return false;
1✔
95
      }
96
      tablesToSkip = args[i + 1];
2✔
97
      i += 1;
2✔
98
      continue;
2✔
99
    }
100
    else if ( args[i] == "--include-tables" )
2✔
101
    {
NEW
102
      if ( i + 1 >= args.size() )
×
103
      {
NEW
104
        std::cout << "Error: missing arguments for include-tables option" << std::endl;
×
NEW
105
        return false;
×
106
      }
NEW
107
      tablesToInclude = args[i + 1];
×
NEW
108
      i += 1;
×
NEW
109
      continue;
×
110
    }
111
    else
112
    {
113
      std::cout << "Error: unknown option '" << args[i] << "' for '" << cmdName << "' command." << std::endl;
2✔
114
      return false;
2✔
115
    }
116
  }
117
  return true;
33✔
118
}
119

120
static std::vector<std::string> parseIgnoredTables( std::string &tablesToSkip )
4✔
121
{
122
  std::vector<std::string> tables;
4✔
123

124
  std::istringstream strm( tablesToSkip );
4✔
125
  std::string s;
4✔
126
  while ( getline( strm, s, ';' ) )
8✔
127
  {
128
    tables.push_back( s );
4✔
129
  }
130

131
  return tables;
8✔
132
}
4✔
133

134

135
static int handleCmdDiff( GEODIFF_ContextH context, const std::vector<std::string> &args )
18✔
136
{
137
  //   geodiff diff [OPTIONS...] DB_1 DB_2 [CH_OUTPUT]
138

139
  bool writeJson = false;
18✔
140
  bool writeSummary = false;
18✔
141
  bool printOutput = true;
18✔
142
  std::string db1, db2, chOutput;
18✔
143
  std::string driver1Name = "sqlite", driver2Name = "sqlite", driver1Options, driver2Options;
72✔
144
  std::string tablesToSkip, tablesToInclude;
18✔
145
  size_t i = 1;
18✔
146

147
  // parse options
148
  for ( ; i < args.size(); ++i )
31✔
149
  {
150
    if ( !isOption( args[i] ) )
30✔
151
      break;  // no more options
12✔
152

153
    if ( args[i] == "--json" )
18✔
154
    {
155
      writeJson = true;
4✔
156
      continue;
4✔
157
    }
158
    else if ( args[i] == "--summary" )
14✔
159
    {
160
      writeSummary = true;
4✔
161
      continue;
4✔
162
    }
163
    else if ( args[i] == "--driver" || args[i] == "--driver-1" || args[i] == "--driver-2" )
10✔
164
    {
165
      if ( i + 2 >= args.size() )
3✔
166
      {
167
        std::cout << "Error: missing arguments for driver option" << std::endl;
×
168
        return 1;
×
169
      }
170
      if ( args[i] == "--driver" || args[i] == "--driver-1" )
3✔
171
      {
172
        driver1Name = args[i + 1];
2✔
173
        driver1Options = args[i + 2];
2✔
174
      }
175
      if ( args[i] == "--driver" || args[i] == "--driver-2" )
3✔
176
      {
177
        driver2Name = args[i + 1];
2✔
178
        driver2Options = args[i + 2];
2✔
179
      }
180
      i += 2;
3✔
181
      continue;
3✔
182
    }
183
    else if ( args[i] == "--skip-tables" )
7✔
184
    {
185
      if ( i + 1 >= args.size() )
3✔
186
      {
187
        std::cout << "Error: missing arguments for skip-tables option" << std::endl;
1✔
188
        return 1;
1✔
189
      }
190
      tablesToSkip = args[i + 1];
2✔
191
      i += 1;
2✔
192
      continue;
2✔
193
    }
194
    else if ( args[i] == "--include-tables" )
4✔
195
    {
NEW
196
      if ( i + 1 >= args.size() )
×
197
      {
NEW
198
        std::cout << "Error: missing arguments for include-tables option" << std::endl;
×
NEW
199
        return 1;
×
200
      }
NEW
201
      tablesToInclude = args[i + 1];
×
NEW
202
      i += 1;
×
NEW
203
      continue;
×
204
    }
205
    else
206
    {
207
      std::cout << "Error: unknown option '" << args[i] << "' for 'diff' command." << std::endl;
4✔
208
      return 1;
4✔
209
    }
210
  }
211

212
  // check validity of options
213
  if ( writeJson && writeSummary )
13✔
214
  {
215
    std::cout << "Error: only one of the options can be passed: --json or --summary" << std::endl;
1✔
216
    return 1;
1✔
217
  }
218

219
  if ( !tablesToSkip.empty() && !tablesToInclude.empty() )
12✔
220
  {
NEW
221
    std::cout << "Error: only one of --skip-tables or --include-tables can be used at a time." << std::endl;
×
NEW
222
    return 1;
×
223
  }
224

225
  Context *ctx = static_cast<Context *>( context );
12✔
226
  if ( !tablesToSkip.empty() )
12✔
227
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
2✔
228
  else if ( !tablesToInclude.empty() )
10✔
NEW
229
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
230

231
  // parse required arguments
232
  if ( !parseRequiredArgument( db1, args, i, "DB_1", "diff" ) )
48✔
233
    return 1;
1✔
234
  if ( !parseRequiredArgument( db2, args, i, "DB_2", "diff" ) )
44✔
235
    return 1;
2✔
236

237
  if ( i < args.size() )
9✔
238
  {
239
    // optional output argument
240
    printOutput = false;
2✔
241
    chOutput = args[i++];
2✔
242

243
    if ( !checkNoExtraArguments( args, i, "diff" ) )
4✔
244
      return 1;
1✔
245
  }
246

247
  std::string changeset;
8✔
248
  TmpFile tmpChangeset;
8✔
249
  if ( printOutput || writeJson || writeSummary )
8✔
250
  {
251
    changeset = randomTmpFilename( );
7✔
252
    tmpChangeset.setPath( changeset );
7✔
253
  }
254
  else
255
    changeset = chOutput;
1✔
256

257
  if ( driver1Name == driver2Name && driver1Options == driver2Options )
8✔
258
  {
259
    int ret = GEODIFF_createChangesetEx( context,
8✔
260
                                         driver1Name.data(), driver1Options.data(),
8✔
261
                                         db1.data(), db2.data(), changeset.data() );
8✔
262
    if ( ret != GEODIFF_SUCCESS )
8✔
263
    {
264
      std::cout << "Error: diff failed!" << std::endl;
1✔
265
      return 1;
1✔
266
    }
267
  }
268
  else
269
  {
270
    int ret = GEODIFF_createChangesetDr( context,
×
271
                                         driver1Name.data(), driver1Options.data(), db1.data(),
×
272
                                         driver2Name.data(), driver2Options.data(), db2.data(),
×
273
                                         changeset.data() );
×
274
    if ( ret != GEODIFF_SUCCESS )
×
275
    {
276
      std::cout << "Error: diff failed!" << std::endl;
×
277
      return 1;
×
278
    }
279
  }
280

281
  if ( writeJson || writeSummary )
7✔
282
  {
283
    std::string json;
5✔
284
    TmpFile tmpJson;
5✔
285
    if ( printOutput )
5✔
286
    {
287
      json = randomTmpFilename( );
5✔
288
      tmpJson.setPath( json );
5✔
289
    }
290
    else
291
      json = chOutput;
×
292

293
    if ( writeJson )
5✔
294
    {
295
      if ( GEODIFF_listChanges( context, changeset.data(), json.data() ) != GEODIFF_SUCCESS )
2✔
296
      {
297
        std::cout << "Error: failed to convert changeset to JSON!" << std::endl;
×
298
        return 1;
×
299
      }
300
    }
301
    else  // writeSummary
302
    {
303
      if ( GEODIFF_listChangesSummary( context, changeset.data(), json.data() ) != GEODIFF_SUCCESS )
3✔
304
      {
305
        std::cout << "Error: failed to convert changeset to summary!" << std::endl;
×
306
        return 1;
×
307
      }
308
    }
309

310
    if ( printOutput )
5✔
311
    {
312
      if ( !fileToStdout( json ) )
5✔
313
      {
314
        std::cout << "Error: unable to read content of file " << json << std::endl;
×
315
        return 1;
×
316
      }
317
    }
318
  }
10✔
319
  else if ( printOutput )
2✔
320
  {
321
    if ( !fileToStdout( changeset ) )
1✔
322
    {
323
      std::cout << "Error: unable to read content of file " << changeset << std::endl;
×
324
      return 1;
×
325
    }
326
  }
327

328
  return 0;
7✔
329
}
18✔
330

331

332
static int handleCmdApply( GEODIFF_ContextH context, const std::vector<std::string> &args )
13✔
333
{
334
  // geodiff apply [OPTIONS...] DB CH_INPUT
335

336
  size_t i = 1;
13✔
337
  std::string db, changeset;
13✔
338
  std::string driverName = "sqlite", driverOptions;
26✔
339
  std::string tablesToSkip, tablesToInclude;
13✔
340

341
  // parse options
342
  if ( !parseDriverOption( args, i, "apply", driverName, driverOptions, tablesToSkip, tablesToInclude ) )
26✔
343
    return 1;
3✔
344

345
  // parse required arguments
346
  if ( !parseRequiredArgument( db, args, i, "DB", "apply" ) )
40✔
347
    return 1;
2✔
348
  if ( !parseRequiredArgument( changeset, args, i, "CH_INPUT", "apply" ) )
32✔
349
    return 1;
3✔
350
  if ( !checkNoExtraArguments( args, i, "apply" ) )
10✔
351
    return 1;
2✔
352

353
  Context *ctx = static_cast<Context *>( context );
3✔
354
  if ( !tablesToSkip.empty() )
3✔
355
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
1✔
356
  else if ( !tablesToInclude.empty() )
2✔
NEW
357
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
358

359
  int ret = GEODIFF_applyChangesetEx( context, driverName.data(), driverOptions.data(), db.data(), changeset.data() );
3✔
360
  if ( ret != GEODIFF_SUCCESS )
3✔
361
  {
362
    std::cout << "Error: apply changeset failed!" << std::endl;
1✔
363
    return 1;
1✔
364
  }
365

366
  return 0;
2✔
367
}
13✔
368

369
static int handleCmdRebaseDiff( GEODIFF_ContextH context,  const std::vector<std::string> &args )
10✔
370
{
371
  // geodiff rebase-diff [OPTIONS...] DB_BASE CH_BASE_OUR CH_BASE_THEIR CH_REBASED CONFLICT
372

373
  size_t i = 1;
10✔
374
  std::string dbBase, chBaseOur, chBaseTheir, chRebased, conflict;
10✔
375
  std::string driverName = "sqlite", driverOptions;
20✔
376
  std::string tablesToSkip, tablesToInclude;
10✔
377

378
  // parse options
379
  if ( !parseDriverOption( args, i, "rebase-diff", driverName, driverOptions, tablesToSkip, tablesToInclude ) )
20✔
380
    return 1;
2✔
381

382
  if ( !parseRequiredArgument( dbBase, args, i, "DB_BASE", "rebase-diff" ) )
32✔
383
    return 1;
1✔
384
  if ( !parseRequiredArgument( chBaseOur, args, i, "CH_BASE_OUR", "rebase-diff" ) )
28✔
385
    return 1;
1✔
386
  if ( !parseRequiredArgument( chBaseTheir, args, i, "CH_BASE_THEIR", "rebase-diff" ) )
24✔
387
    return 1;
1✔
388
  if ( !parseRequiredArgument( chRebased, args, i, "CH_REBASED", "rebase-diff" ) )
20✔
389
    return 1;
×
390
  if ( !parseRequiredArgument( conflict, args, i, "CONFLICT", "rebase-diff" ) )
20✔
391
    return 1;
1✔
392
  if ( !checkNoExtraArguments( args, i, "rebase-diff" ) )
8✔
393
    return 1;
1✔
394

395
  Context *ctx = static_cast<Context *>( context );
3✔
396
  if ( !tablesToSkip.empty() )
3✔
NEW
397
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
×
398
  else if ( !tablesToInclude.empty() )
3✔
NEW
399
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
400

401
  int ret = GEODIFF_createRebasedChangesetEx(
3✔
402
              context,
403
              driverName.data(), driverOptions.data(),
3✔
404
              dbBase.data(), chBaseOur.data(),
3✔
405
              chBaseTheir.data(), chRebased.data(), conflict.data() );
3✔
406
  if ( ret != GEODIFF_SUCCESS )
3✔
407
  {
408
    std::cout << "Error: rebase-diff failed!" << std::endl;
2✔
409
    return 1;
2✔
410
  }
411

412
  return 0;
1✔
413
}
10✔
414

415
static int handleCmdRebaseDb( GEODIFF_ContextH context, const std::vector<std::string> &args )
9✔
416
{
417
  // geodiff rebase-db [OPTIONS...] DB_BASE DB_OUR CH_BASE_THEIR CONFLICT
418

419
  size_t i = 1;
9✔
420
  std::string dbBase, dbOur, chBaseTheir, conflict;
9✔
421
  std::string driverName = "sqlite", driverOptions;
18✔
422
  std::string tablesToSkip, tablesToInclude;
9✔
423

424
  // parse options
425
  if ( !parseDriverOption( args, i, "rebase-db", driverName, driverOptions, tablesToSkip, tablesToInclude ) )
18✔
426
    return 1;
3✔
427

428
  if ( !parseRequiredArgument( dbBase, args, i, "DB_BASE", "rebase-db" ) )
24✔
429
    return 1;
1✔
430
  if ( !parseRequiredArgument( dbOur, args, i, "DB_OUR", "rebase-db" ) )
20✔
431
    return 1;
×
432
  if ( !parseRequiredArgument( chBaseTheir, args, i, "CH_BASE_THEIR", "rebase-db" ) )
20✔
433
    return 1;
×
434
  if ( !parseRequiredArgument( conflict, args, i, "CONFLICT", "rebase-db" ) )
20✔
435
    return 1;
1✔
436
  if ( !checkNoExtraArguments( args, i, "rebase-db" ) )
8✔
437
    return 1;
×
438

439
  Context *ctx = static_cast<Context *>( context );
4✔
440
  if ( !tablesToSkip.empty() )
4✔
NEW
441
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
×
442
  else if ( !tablesToInclude.empty() )
4✔
NEW
443
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
444

445
  int ret = GEODIFF_rebaseEx( context,
4✔
446
                              driverName.data(), driverOptions.data(), dbBase.data(), dbOur.data(),
4✔
447
                              chBaseTheir.data(), conflict.data() );
4✔
448
  if ( ret != GEODIFF_SUCCESS )
4✔
449
  {
450
    std::cout << "Error: rebase-db failed!" << std::endl;
3✔
451
    return 1;
3✔
452
  }
453

454
  return 0;
1✔
455
}
9✔
456

457
static int handleCmdInvert( GEODIFF_ContextH context, const std::vector<std::string> &args )
6✔
458
{
459
  // geodiff invert CH_INPUT CH_OUTPUT
460

461
  size_t i = 1;
6✔
462
  std::string chInput, chOutput;
6✔
463

464
  if ( !parseRequiredArgument( chInput, args, i, "CH_INPUT", "invert" ) )
24✔
465
    return 1;
1✔
466
  if ( !parseRequiredArgument( chOutput, args, i, "CH_OUTPUT", "invert" ) )
20✔
467
    return 1;
2✔
468
  if ( !checkNoExtraArguments( args, i, "invert" ) )
6✔
469
    return 1;
1✔
470

471
  int ret = GEODIFF_invertChangeset( context, chInput.data(), chOutput.data() );
2✔
472
  if ( ret != GEODIFF_SUCCESS )
2✔
473
  {
474
    std::cout << "Error: invert changeset failed!" << std::endl;
1✔
475
    return 1;
1✔
476
  }
477

478
  return 0;
1✔
479
}
6✔
480

481
static int handleCmdConcat( GEODIFF_ContextH context, const std::vector<std::string> &args )
4✔
482
{
483
  // geodiff concat CH_INPUT_1 CH_INPUT_2 [...] CH_OUTPUT
484

485
  if ( args.size() < 4 )
4✔
486
  {
487
    std::cout << "Error: 'concat' command needs at least two input changesets and one output changeset." << std::endl;
1✔
488
    return 1;
1✔
489
  }
490

491
  std::string chOutput = args[args.size() - 1];
3✔
492

493
  std::vector<const char *> changesets;
3✔
494
  for ( size_t i = 1; i < args.size() - 1; ++i )
10✔
495
  {
496
    changesets.push_back( args[i].data() );
7✔
497
  }
498

499
  int ret = GEODIFF_concatChanges( context, ( int ) changesets.size(), changesets.data(), chOutput.data() );
3✔
500
  if ( ret != GEODIFF_SUCCESS )
3✔
501
  {
502
    std::cout << "Error: concat changesets failed!" << std::endl;
1✔
503
    return 1;
1✔
504
  }
505

506
  return 0;
2✔
507
}
3✔
508

509
static int handleCmdAsJson( GEODIFF_ContextH context, const std::vector<std::string> &args )
8✔
510
{
511
  // geodiff as-json CH_INPUT [CH_OUTPUT]
512

513
  size_t i = 1;
8✔
514
  bool printOutput = true;
8✔
515
  std::string chInput, chOutput;
8✔
516

517
  // parse required arguments
518
  if ( !parseRequiredArgument( chInput, args, i, "CH_INPUT", "as-json" ) )
32✔
519
    return 1;
1✔
520

521
  if ( i < args.size() )
7✔
522
  {
523
    // optional output argument
524
    printOutput = false;
3✔
525
    chOutput = args[i++];
3✔
526

527
    if ( !checkNoExtraArguments( args, i, "as-json" ) )
6✔
528
      return 1;
1✔
529
  }
530

531
  std::string changeset;
6✔
532
  TmpFile tmpChangeset;
6✔
533
  if ( printOutput )
6✔
534
  {
535
    changeset = randomTmpFilename( );
4✔
536
    tmpChangeset.setPath( changeset );
4✔
537
  }
538
  else
539
    changeset = chOutput;
2✔
540

541
  int ret = GEODIFF_listChanges( context, chInput.data(), changeset.data() );
6✔
542
  if ( ret != GEODIFF_SUCCESS )
6✔
543
  {
544
    std::cout << "Error: export changeset to JSON failed!" << std::endl;
1✔
545
    return 1;
1✔
546
  }
547

548
  if ( printOutput )
5✔
549
  {
550
    if ( !fileToStdout( changeset ) )
4✔
551
    {
552
      std::cout << "Error: unable to read content of file " << changeset << std::endl;
×
553
      return 1;
×
554
    }
555
  }
556

557
  return 0;
5✔
558
}
8✔
559

560
static int handleCmdAsSummary( GEODIFF_ContextH context, const std::vector<std::string> &args )
4✔
561
{
562
  // geodiff as-summary CH_INPUT [SUMMARY]
563

564
  size_t i = 1;
4✔
565
  bool printOutput = true;
4✔
566
  std::string chInput, chOutput;
4✔
567

568
  // parse required arguments
569
  if ( !parseRequiredArgument( chInput, args, i, "CH_INPUT", "as-summary" ) )
16✔
570
    return 1;
1✔
571

572
  if ( i < args.size() )
3✔
573
  {
574
    // optional output argument
575
    printOutput = false;
2✔
576
    chOutput = args[i++];
2✔
577

578
    if ( !checkNoExtraArguments( args, i, "as-summary" ) )
4✔
579
      return 1;
×
580
  }
581

582
  std::string summary;
3✔
583
  TmpFile tmpSummary;
3✔
584
  if ( printOutput )
3✔
585
  {
586
    summary = randomTmpFilename( );
1✔
587
    tmpSummary.setPath( summary );
1✔
588
  }
589
  else
590
    summary = chOutput;
2✔
591

592
  int ret = GEODIFF_listChangesSummary( context, chInput.data(), summary.data() );
3✔
593
  if ( ret != GEODIFF_SUCCESS )
3✔
594
  {
595
    std::cout << "Error: export changeset to summary failed!" << std::endl;
1✔
596
    return 1;
1✔
597
  }
598

599
  if ( printOutput )
2✔
600
  {
601
    if ( !fileToStdout( summary ) )
1✔
602
    {
603
      std::cout << "Error: unable to read content of file " << summary << std::endl;
×
604
      return 1;
×
605
    }
606
  }
607

608
  return 0;
2✔
609
}
4✔
610

611
static int handleCmdCopy( GEODIFF_ContextH context, const std::vector<std::string> &args )
12✔
612
{
613
  // geodiff copy [OPTIONS...] DB_SOURCE DB_DESTINATION
614

615
  size_t i = 1;
12✔
616
  std::string chInput, chOutput;
12✔
617
  std::string driver1Name = "sqlite", driver1Options, driver2Name = "sqlite", driver2Options;
48✔
618
  std::string tablesToSkip, tablesToInclude;
12✔
619

620
  // parse options
621
  for ( ; i < args.size(); ++i )
17✔
622
  {
623
    if ( !isOption( args[i] ) )
14✔
624
      break;  // no more options
9✔
625

626
    if ( args[i] == "--driver" || args[i] == "--driver-1" || args[i] == "--driver-2" )
5✔
627
    {
628
      if ( i + 2 >= args.size() )
3✔
629
      {
630
        std::cout << "Error: missing arguments for driver option" << std::endl;
×
631
        return 1;
×
632
      }
633
      if ( args[i] == "--driver" || args[i] == "--driver-1" )
3✔
634
      {
635
        driver1Name = args[i + 1];
2✔
636
        driver1Options = args[i + 2];
2✔
637
      }
638
      if ( args[i] == "--driver" || args[i] == "--driver-2" )
3✔
639
      {
640
        driver2Name = args[i + 1];
2✔
641
        driver2Options = args[i + 2];
2✔
642
      }
643
      i += 2;
3✔
644
      continue;
3✔
645
    }
646
    else if ( args[i] == "--skip-tables" )
2✔
647
    {
648
      if ( i + 1 >= args.size() )
2✔
649
      {
650
        std::cout << "Error: missing arguments for skip-tables option" << std::endl;
×
651
        return 1;
×
652
      }
653
      tablesToSkip = args[i + 1];
2✔
654
      i += 1;
2✔
655
      continue;
2✔
656
    }
NEW
657
    else if ( args[i] == "--include-tables" )
×
658
    {
NEW
659
      if ( i + 1 >= args.size() )
×
660
      {
NEW
661
        std::cout << "Error: missing arguments for include-tables option" << std::endl;
×
NEW
662
        return 1;
×
663
      }
NEW
664
      tablesToInclude = args[i + 1];
×
NEW
665
      i += 1;
×
NEW
666
      continue;
×
667
    }
668
    else
669
    {
670
      std::cout << "Error: unknown option '" << args[i] << "' for 'copy' command." << std::endl;
×
671
      return 1;
×
672
    }
673
  }
674

675
  if ( !parseRequiredArgument( chInput, args, i, "DB_SOURCE", "copy" ) )
48✔
676
    return 1;
3✔
677
  if ( !parseRequiredArgument( chOutput, args, i, "DB_DESTINATION", "copy" ) )
36✔
678
    return 1;
1✔
679
  if ( !checkNoExtraArguments( args, i, "copy" ) )
16✔
680
    return 1;
1✔
681

682
  Context *ctx = static_cast<Context *>( context );
7✔
683
  if ( !tablesToSkip.empty() )
7✔
684
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
1✔
685
  else if ( !tablesToInclude.empty() )
6✔
NEW
686
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
687

688
  if ( driver1Name == "sqlite" && driver2Name == "sqlite" )
7✔
689
  {
690
    if ( !tablesToSkip.empty() || !tablesToInclude.empty() )
7✔
691
    {
692
      std::cout << "Source and destination drivers are \"sqlite\". Table filter options will be ignored." << std::endl;
1✔
693
      return 1;
1✔
694
    }
695

696
    int ret = GEODIFF_makeCopySqlite( context, chInput.data(), chOutput.data() );
6✔
697
    if ( ret != GEODIFF_SUCCESS )
6✔
698
    {
699
      std::cout << "Error: copy failed!" << std::endl;
1✔
700
      return 1;
1✔
701
    }
702
  }
703
  else
704
  {
705
    int ret = GEODIFF_makeCopy( context,
×
706
                                driver1Name.data(), driver1Options.data(), chInput.data(),
×
707
                                driver2Name.data(), driver2Options.data(), chOutput.data() );
×
708
    if ( ret != GEODIFF_SUCCESS )
×
709
    {
710
      std::cout << "Error: copy failed!" << std::endl;
×
711
      return 1;
×
712
    }
713
  }
714

715
  return 0;
5✔
716
}
12✔
717

718
static int handleCmdSchema( GEODIFF_ContextH context, const std::vector<std::string> &args )
5✔
719
{
720
  // geodiff schema [OPTIONS...] DB [SCHEMA_JSON]
721

722
  size_t i = 1;
5✔
723
  bool printOutput = true;
5✔
724
  std::string db, schemaJson;
5✔
725
  std::string driverName = "sqlite", driverOptions;
10✔
726
  std::string tablesToSkip, tablesToInclude;
5✔
727

728
  // parse options
729
  if ( !parseDriverOption( args, i, "schema", driverName, driverOptions, tablesToSkip, tablesToInclude ) )
10✔
730
    return 1;
×
731

732
  // parse required arguments
733
  if ( !parseRequiredArgument( db, args, i, "DB", "schema" ) )
20✔
734
    return 1;
1✔
735

736
  if ( i < args.size() )
4✔
737
  {
738
    // optional output argument
739
    printOutput = false;
2✔
740
    schemaJson = args[i++];
2✔
741

742
    if ( !checkNoExtraArguments( args, i, "schema" ) )
4✔
743
      return 1;
1✔
744
  }
745

746
  Context *ctx = static_cast<Context *>( context );
3✔
747
  if ( !tablesToSkip.empty() )
3✔
NEW
748
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
×
749
  else if ( !tablesToInclude.empty() )
3✔
NEW
750
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
751

752
  std::string json;
3✔
753
  TmpFile tmpJson;
3✔
754
  if ( printOutput )
3✔
755
  {
756
    json = randomTmpFilename( );
2✔
757
    tmpJson.setPath( json );
2✔
758
  }
759
  else
760
    json = schemaJson;
1✔
761

762
  int ret = GEODIFF_schema( context, driverName.data(), driverOptions.data(), db.data(), json.data() );
3✔
763
  if ( ret != GEODIFF_SUCCESS )
3✔
764
  {
765
    std::cout << "Error: export changeset to summary failed!" << std::endl;
1✔
766
    return 1;
1✔
767
  }
768

769
  if ( printOutput )
2✔
770
  {
771
    if ( !fileToStdout( json ) )
1✔
772
    {
773
      std::cout << "Error: unable to read content of file " << json << std::endl;
×
774
      return 1;
×
775
    }
776
  }
777

778
  return 0;
2✔
779
}
5✔
780

781
static int handleCmdDump( GEODIFF_ContextH context, const std::vector<std::string> &args )
6✔
782
{
783
  // geodiff dump [OPTIONS...] DB CH_OUTPUT
784

785
  size_t i = 1;
6✔
786
  std::string db, chOutput;
6✔
787
  std::string driverName = "sqlite", driverOptions;
12✔
788
  std::string tablesToSkip, tablesToInclude;
6✔
789

790
  // parse options
791
  if ( !parseDriverOption( args, i, "dump", driverName, driverOptions, tablesToSkip, tablesToInclude ) )
12✔
792
    return 1;
2✔
793

794
  if ( !parseRequiredArgument( db, args, i, "DB", "dump" ) )
16✔
795
    return 1;
1✔
796
  if ( !parseRequiredArgument( chOutput, args, i, "CH_OUTPUT", "dump" ) )
12✔
797
    return 1;
1✔
798
  if ( !checkNoExtraArguments( args, i, "dump" ) )
4✔
799
    return 1;
1✔
800

801
  Context *ctx = static_cast<Context *>( context );
1✔
802
  if ( !tablesToSkip.empty() )
1✔
NEW
803
    ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) );
×
804
  else if ( !tablesToInclude.empty() )
1✔
NEW
805
    ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) );
×
806

807
  int ret = GEODIFF_dumpData( context, driverName.data(), driverOptions.data(), db.data(), chOutput.data() );
1✔
808
  if ( ret != GEODIFF_SUCCESS )
1✔
809
  {
810
    std::cout << "Error: dump database failed!" << std::endl;
×
811
    return 1;
×
812
  }
813

814
  return 0;
1✔
815
}
6✔
816

817
static int handleCmdDrivers( const std::vector<std::string> &args )
2✔
818
{
819
  // geodiff drivers
820

821
  size_t i = 1;
2✔
822
  if ( !checkNoExtraArguments( args, i, "drivers" ) )
4✔
823
    return 1;
1✔
824

825
  for ( const std::string &driverName : Driver::drivers() )
3✔
826
  {
827
    std::cout << driverName << std::endl;
2✔
828
  }
1✔
829

830
  return 0;
1✔
831
}
832

833
static int handleCmdVersion( const std::vector<std::string> &args )
3✔
834
{
835
  size_t i = 1;
3✔
836
  if ( !checkNoExtraArguments( args, i, "version" ) )
6✔
837
    return 1;
1✔
838

839
  std::cout << GEODIFF_version() << std::endl;
2✔
840
  return 0;
2✔
841
}
842

843
static GEODIFF_LoggerLevel getGeodiffLoggerLevel( )
104✔
844
{
845
  return ( GEODIFF_LoggerLevel ) getEnvVarInt( "GEODIFF_LOGGER_LEVEL", ( int ) LevelWarning );
208✔
846
}
847

848
static int handleCmdHelp( const std::vector<std::string> &args )
2✔
849
{
850
  size_t i = 1;
2✔
851
  if ( !checkNoExtraArguments( args, i, "help" ) )
4✔
852
    return 1;
1✔
853

854

855
  std::cout << "GEODIFF " << GEODIFF_version() << ", a tool for handling diffs for geospatial data.\n\
1✔
856
\n\
857
Usage: geodiff <command> [args...]\n\
858
\n\
859
You can control verbosity using the environment variable GEODIFF_LOGGER_LEVEL:\n\
860
    0 = Nothing, 1 = Errors, 2 = Warnings, 3 = Info, 4 = Debug\n\
861
    (The default is 2 - showing only errors and warnings.)\n\
862
\n\
863
In the commands listed below, database files may be any GeoPackage files or other\n\
864
kinds of SQLite database files. This is using the default 'sqlite' driver. Even\n\
865
when 'sqlite' driver is specified in a command with --driver option, there are\n\
866
no extra driver options it needs (empty string \"\" can be passed).\n\
867
\n\
868
There may be other drivers available, for example 'postgres' driver. Its driver\n\
869
options expect the connection string as understood by its client library - either\n\
870
key/value pairs (e.g. \"host=localhost port=5432 dbname=mydb\") or connection URI\n\
871
(e.g. \"postgresql://localhost:5432/mydb\").\n\
872
\n\
873
Create and apply changesets (diffs):\n\
874
\n\
875
  geodiff diff [OPTIONS...] DB_1 DB_2 [CH_OUTPUT]\n\
876
\n\
877
    Creates a changeset (diff) between databases DB_BASE and DB_MODIFIED. If CH_OUTPUT\n\
878
    is specified, the result is written to that file, otherwise the output goes\n\
879
    to the standard output. By default, the changeset is written in the binary\n\
880
    format.\n\
881
\n\
882
    Options:\n\
883
      --json          Write changeset in JSON format instead of binary\n\
884
      --summary       Write only a summary for each table (in JSON)\n\
885
      --driver NAME DRIVER_OPTIONS\n\
886
                      Use driver NAME instead of the default 'sqlite' for both\n\
887
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
888
      --driver-1 NAME DRIVER_OPTIONS\n\
889
                      Use driver NAME just for the first database. This allows creation\n\
890
                      of changesets across datasets in two different drivers.\n\
891
      --driver-2 NAME DRIVER_OPTIONS\n\
892
                      Use driver NAME just for the second database. This allows\n\
893
                      creation of changesets across datasets in two different drivers.\n\
894
      --skip-tables TABLES\n\
895
                      Ignore specified tables when creating a changeset. Tables are defined as\n\
896
                      a semicolon separated list of names.\n\
897
\n\
898
  geodiff apply [OPTIONS...] DB CH_INPUT\n\
899
\n\
900
    Applies a changeset (diff) from file CH_INPUT to the database file DB.\n\
901
    The changeset must be in the binary format (JSON format is not supported).\n\
902
\n\
903
    Options:\n\
904
      --driver NAME DRIVER_OPTIONS\n\
905
                      Use driver NAME instead of the default 'sqlite' for the\n\
906
                      database. Driver-specific options are provided in CONN_OPTIONS.\n\
907
      --skip-tables TABLES\n\
908
                      Ignore specified tables when applying a changeset. Tables are defined as\n\
909
                      a semicolon separated list of names.\n\
910
\n\
911
Rebasing:\n\
912
\n\
913
  geodiff rebase-diff [OPTIONS...] DB_BASE CH_BASE_OUR CH_BASE_THEIR CH_REBASED CONFLICT\n\
914
\n\
915
    Creates a rebased changeset. Using DB_BASE as the common base for \"our\" local\n\
916
    changes (CH_BASE_OUR) and \"their\" changes (CH_BASE_THEIR), the command will take\n\
917
    \"our\" changes and rebase them on top of \"their\" changes, and write results\n\
918
    to CH_REBASED file (containing just \"our\" changes, but modified to apply cleanly\n\
919
    on top of \"their\" changes). As a result, taking DB_BASE, applying CH_BASE_THEIR\n\
920
    and then applying CH_REBASED will result in a database containing both \"our\" and\n\
921
    \"their\" changes. If there were any conflicts during the rebase, they will be\n\
922
    written to CONFLICT file (in JSON format).\n\
923
\n\
924
    Options:\n\
925
      --driver NAME DRIVER_OPTIONS\n\
926
                      Use driver NAME instead of the default 'sqlite' for both\n\
927
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
928
      --skip-tables TABLES\n\
929
                      Ignore specified tables when creating a rebased changeset. Tables are\n\
930
                      defined as a semicolon separated list of names.\n\
931
\n\
932
  geodiff rebase-db [OPTIONS...] DB_BASE DB_OUR CH_BASE_THEIR CONFLICT\n\
933
\n\
934
    Rebases database DB_OUR, using DB_BASE as the common base and CH_BASE_THEIR as the other\n\
935
    source of changes. CH_BASE_THEIR is a changeset containing changes between DB_BASE and\n\
936
    some other database. This will cause DB_OUR to be updated in-place to contain changes\n\
937
    (DB_OUR - DB_BASE) rebased on top of CH_BASE_THEIR. After successful rebase, DB_OUR will\n\
938
    contain both \"our\" and \"their\" changes. If there were any conflicts during\n\
939
    the rebase, they will be written to CONFLICT file (in JSON format).\n\
940
\n\
941
    Options:\n\
942
      --driver NAME DRIVER_OPTIONS\n\
943
                      Use driver NAME instead of the default 'sqlite' for all three\n\
944
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
945
      --skip-tables TABLES\n\
946
                      Ignore specified tables when rebasing. Tables are defined as\n\
947
                      a semicolon separated list of names.\n\
948
\n\
949
Utilities:\n\
950
\n\
951
  geodiff invert CH_INPUT CH_OUTPUT\n\
952
\n\
953
    Inverts changeset in file CH_INPUT and writes inverted changeset to CH_OUTPUT.\n\
954
    Both input and output changesets are in the binary format.\n\
955
\n\
956
  geodiff concat CH_INPUT_1 CH_INPUT_2 [...] CH_OUTPUT\n\
957
\n\
958
    Concatenates two or more changeset files (CH_INPUT_1, CH_INPUT_2, ...) into a\n\
959
    single changeset. During concatenation, commands that act on the same rows get\n\
960
    merged together.\n\
961
\n\
962
  geodiff as-json CH_INPUT [CH_OUTPUT]\n\
963
\n\
964
    Converts the changeset in CH_INPUT file (in binary format) to JSON representation.\n\
965
    If CH_OUTPUT file is provided, it will be written to that file, otherwise it will\n\
966
    be written to the standard output.\n\
967
\n\
968
  geodiff as-summary CH_INPUT [SUMMARY]\n\
969
\n\
970
    Converts the changeset in CH_INPUT file (in binary format) to a summary JSON\n\
971
    which only contains overall counts of insert/update/delete commands for each table.\n\
972
\n\
973
  geodiff copy [OPTIONS...] DB_SOURCE DB_DESTINATION\n\
974
\n\
975
    Copies the source database DB_SOURCE to the destination database DB_DESTINATION.\n\
976
\n\
977
    Options:\n\
978
      --driver NAME DRIVER_OPTIONS\n\
979
                      Use driver NAME instead of the default 'sqlite' for both\n\
980
                      databases. Driver-specific options are provided in CONN_OPTIONS.\n\
981
      --driver-1 NAME DRIVER_OPTIONS\n\
982
                      Use driver NAME just for the first database. This allows creation\n\
983
                      of changesets across datasets in two different drivers.\n\
984
      --driver-2 NAME DRIVER_OPTIONS\n\
985
                      Use driver NAME just for the second database. This allows\n\
986
                      creation of changesets across datasets in two different drivers.\n\
987
      --skip-tables TABLES\n\
988
                      Ignore specified tables when copying the database. Tables are defined\n\
989
                      as a semicolon separated list of names.\n\
990
\n\
991
  geodiff schema [OPTIONS...] DB [SCHEMA_JSON]\n\
992
\n\
993
    Writes database schema of DB as understood by geodiff as JSON. If SCHEMA_JSON file\n\
994
    is provided, the output will be written to the file, otherwise the standard output\n\
995
    will be used.\n\
996
\n\
997
    Options:\n\
998
      --driver NAME DRIVER_OPTIONS\n\
999
                      Use driver NAME instead of the default 'sqlite' for the\n\
1000
                      database. Driver-specific options are provided in CONN_OPTIONS.\n\
1001
      --skip-tables TABLES\n\
1002
                      Ignore specified tables when writing a schema. Tables are defined\n\
1003
                      as a semicolon separated list of names.\n\
1004
\n\
1005
  geodiff dump [OPTIONS...] DB CH_OUTPUT\n\
1006
\n\
1007
    Dumps content of database DB to a changeset as a series of \"insert\" commands.\n\
1008
\n\
1009
    Options:\n\
1010
      --driver NAME DRIVER_OPTIONS\n\
1011
                      Use driver NAME instead of the default 'sqlite' for the\n\
1012
                      database. Driver-specific options are provided in CONN_OPTIONS.\n\
1013
      --skip-tables TABLES\n\
1014
                      Ignore specified tables when dumping the database content. Tables\n\
1015
                      are defined a semicolon separated list of names.\n\
1016
\n\
1017
  geodiff drivers\n\
1018
\n\
1019
    Prints the list of all drivers supported in this version. The \"sqlite\" driver\n\
1020
    is always available.\n\
1021
\n\
1022
  geodiff version\n\
1023
\n\
1024
    Prints version of geodiff.\n\
1025
\n\
1026
  geodiff help\n\
1027
\n\
1028
    Prints this help information.\n\
1029
\n\
1030
Copyright (C) 2019-2023 Lutra Consulting\n\
1031
";
1032
  return 0;
1✔
1033
}
1034

1035
class GeodiffContext
1036
{
1037
  public:
1038
    GeodiffContext()
104✔
1039
    {
104✔
1040
      mContext = GEODIFF_createContext();
104✔
1041
      GEODIFF_CX_setMaximumLoggerLevel( mContext,  getGeodiffLoggerLevel() );
104✔
1042
    }
104✔
1043
    ~GeodiffContext()
104✔
1044
    {
1045
      if ( mContext )
104✔
1046
      {
1047
        GEODIFF_CX_destroy( mContext );
104✔
1048
        mContext = nullptr;
104✔
1049
      }
1050
    }
104✔
1051
    GEODIFF_ContextH handle() {return mContext;}
95✔
1052

1053
  private:
1054
    GEODIFF_ContextH mContext = nullptr;
1055
};
1056

1057

1058
int main( int argc, char *argv[] )
104✔
1059
{
1060
  GeodiffContext context;
104✔
1061

1062
  std::vector<std::string> args;
104✔
1063
  for ( int i = 1; i < argc; ++i )
446✔
1064
    args.push_back( argv[i] );
684✔
1065

1066
  if ( args.size() < 1 )
104✔
1067
  {
1068
    std::cout << "Error: missing command. See 'geodiff help' for a list of commands." << std::endl;
1✔
1069
    return 1;
1✔
1070
  }
1071

1072
  std::string command = args[0];
103✔
1073
  if ( command == "diff" )
103✔
1074
  {
1075
    return handleCmdDiff( context.handle(), args );
18✔
1076
  }
1077
  else if ( command == "apply" )
85✔
1078
  {
1079
    return handleCmdApply( context.handle(), args );
13✔
1080
  }
1081
  else if ( command == "rebase-diff" )
72✔
1082
  {
1083
    return handleCmdRebaseDiff( context.handle(),  args );
10✔
1084
  }
1085
  else if ( command == "rebase-db" )
62✔
1086
  {
1087
    return handleCmdRebaseDb( context.handle(),  args );
9✔
1088
  }
1089
  else if ( command == "invert" )
53✔
1090
  {
1091
    return handleCmdInvert( context.handle(),  args );
6✔
1092
  }
1093
  else if ( command == "concat" )
47✔
1094
  {
1095
    return handleCmdConcat( context.handle(), args );
4✔
1096
  }
1097
  else if ( command == "as-json" )
43✔
1098
  {
1099
    return handleCmdAsJson( context.handle(),  args );
8✔
1100
  }
1101
  else if ( command == "as-summary" )
35✔
1102
  {
1103
    return handleCmdAsSummary( context.handle(),  args );
4✔
1104
  }
1105
  else if ( command == "copy" )
31✔
1106
  {
1107
    return handleCmdCopy( context.handle(), args );
12✔
1108
  }
1109
  else if ( command == "schema" )
19✔
1110
  {
1111
    return handleCmdSchema( context.handle(),  args );
5✔
1112
  }
1113
  else if ( command == "dump" )
14✔
1114
  {
1115
    return handleCmdDump( context.handle(), args );
6✔
1116
  }
1117
  else if ( command == "drivers" )
8✔
1118
  {
1119
    return handleCmdDrivers( args );
2✔
1120
  }
1121
  else if ( command == "version" )
6✔
1122
  {
1123
    return handleCmdVersion( args );
3✔
1124
  }
1125
  else if ( command == "help" )
3✔
1126
  {
1127
    return handleCmdHelp( args );
2✔
1128
  }
1129
  else
1130
  {
1131
    std::cout << "Error: unknown command '" << command << "'. See 'geodiff help' for a list of commands." << std::endl;
1✔
1132
    return 1;
1✔
1133
  }
1134
}
104✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc