-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce synchronize merge method #1992
Introduce synchronize merge method #1992
Conversation
return false; | ||
} | ||
for (int i = 0; i < m_associations.count(); ++i) { | ||
if (m_associations[i] != other.m_associations[i]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This checks that two lists share the "same" elements in an order-preserving comparison, but I don't think that this is what we want here.
An AutotypeAssociations is a list of Association like [ Association1, Association2, Association3 ]
.
I think the comparison should be order-less.
Should the above example be considered == to [ Association2, Association3, Association1 ]
? What do you guys think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion the above example should only be considered equal when there is no code which depends on the order of the associations. Since it is about auto typing, I assume that is or there may be some feature which does execute the first association by pressing a shortcut - in this case we have to compare order preserving.
I may be dense, but how do you use this feature? I compiled the application and everything seems the same to me. Also the merge function now duplicates folders, including the recycle bin. |
The application should behave the same as before since this pull request mainly considers improvements to existing code. This should allow you to review the changes more easily. The few new features are currently not connected to any UI, so they are only usable writing new code like tests. I'll try to look into the issue regarding the recycle bin - can you describe the merge scenario when this happens? Currently I cannot see anything out of ordinary. The only scenario in which "folders" seem to be duplicated is a merge of two folders which do not share the same history (have different UUID but the same appearance - which seems to be the intended behavior). |
I think this may happen quite regularly with recycle bins. But for this particular case it may be solved by checking which UUID corresponds to the recycle bin in either database and then treat them as equivalent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for you massive contribution. I marked a few things to change which I noticed after a first read-through.
Overall, you contribution is definitely one of the higher-quality ones we have received so far. I think the most important issue is that your patch set contains quite a few unnecessary diffs which only change the formatting of some files. Please remove those. The changes may be correct regarding our style guide, but are not the responsibility of this PR. They would only increase the chance of merge conflicts and inflate the patch set unnecessarily, so please remove them.
It would also be great if you could add a few doc blocks above newly introduced classes and methods briefly stating what they are for and what their parameters are (especially for the more generic interfaces, which might need some explaining).
src/browser/BrowserService.cpp
Outdated
@@ -567,7 +569,8 @@ QList<Entry*> BrowserService::sortEntries(QList<Entry*>& pwEntries, const QStrin | |||
// Sort same priority entries by Title or UserName | |||
auto entries = priorities.values(i); | |||
std::sort(entries.begin(), entries.end(), [&priorities, &field](Entry* left, Entry* right) { | |||
return QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field)) < 0; | |||
return QString::localeAwareCompare(left->attributes()->value(field), right->attributes()->value(field)) | |||
< 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer you don't make so many small formatting changes if they don't improve something substantially. They primarily make merge conflicts more likely.
Moreover, I know there are style guides which dictate a fixed maximum line width, but IMHO breaking only a few characters into a next line is overall less readable than having slightly longer lines at times (especially considering that most people have fairly wide screens these days anyway).
If you break more than just a single term component, it's okay, but this here appears a little excessive to me. If anything, break after the first comma or so.
src/core/AutoTypeAssociations.h
Outdated
@@ -46,6 +46,12 @@ class AutoTypeAssociations : public QObject | |||
int associationsSize() const; | |||
void clear(); | |||
|
|||
bool operator==(const AutoTypeAssociations& other) const; | |||
bool operator!=(const AutoTypeAssociations& other) const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add inline
?
src/core/Clock.cpp
Outdated
*/ | ||
#include "Clock.h" | ||
|
||
static const Clock* m_clock = nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make this a (static) member variable of type QScopedPointer. Don't use raw pointers for long-lived managed freestore objects. You could also use QSharedPointer and then let instance() return that instead of a reference (perhaps even the better because more explicit variant).
|
||
#include <QColor> | ||
|
||
bool operator<(const QColor& lhs, const QColor& rhs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This operator needs a doc block explaining what it does and what the order criterion is. It might also be sensible to move it out of core/ and into gui/.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is needed to compare QColor
properties of entries/groups, so it is not part of gui.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QColor is part of QtGui, so it will pull in the GUI libs. That was my intention. But you're right, Entries already depend on it, so I'm okay with leaving it here for now.
src/core/Compare.h
Outdated
|
||
#include "core/Clock.h" | ||
|
||
enum CompareOption |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might need namespacing or a more specific name that points to its purpose of comparing entries and groups (since the enum members are quite specific to that).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now I don't have any better name for CompareOption. For now it is only a way to collect all possible options for comparisons to prevent scattering and duplicating the logic all over the classes. This way, the client is responsible to select the usable and appropriate members but has consistent behavior and api for comparisons. For now I'm out of ideas for better naming or a better implementation strategy, but I'm open for proposals and improvements in this regard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The members of this enum are quite specific to database items (i.e. groups and entries). Perhaps make that part of the name.
const auto sourceHistoryItems = sourceEntry->historyItems(); | ||
|
||
QMap<QDateTime, Entry*> merged; | ||
for (Entry* historyItem : targetHistoryItems) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the merge result of two histories just be the time-ordered set of both? I don't think individual history items ever change (unless a client has done something shady).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the implemented strategy. It does look more complex since I do some assertions additionally to the needed boilerplate code to prevent unintended database changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not simply use QSets then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QSet
does not allow to specify arbitrary equality operators - it just uses qHash
. Defining qHash
using timestamps (and maybe uuid) only may lead to unintended behavior when defined globally (every QSet
would treat entries equal regardless of object identity) or locally (different semantic of QSet<Entry>
). In my opinion QMap
states the intend more clearly and provides the needed characteristics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, fair point. One last comment: you should perhaps merge lines 376 and 377 into an else if
construct to make it clearer that both conditions are mutually exclusive.
src/core/Merger.h
Outdated
|
||
class Merger | ||
{ | ||
Q_DECLARE_TR_FUNCTIONS(Merger) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to make this a QObject?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QObject is not needed to work properly for the merger - at least not in the current design as short living utility. QObject would add just unnecessary compile time overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the overhead to be problematic compared to the benefits of being able to use the meta object system, signals, slots, and generally more uniformity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simply don't see any benefits. Without inheriting, the API reveals the only intend the class for and how it should be used. The clutter using the meta system somewhat hides this and suggests uses which the class was not designed for. Currently, adjusting to QObject
is trivial, but I'd like to have the person changing to inherit to QObject
reason about the consequences (memory handling, object lifetime, ...). That's why I would prefer Merger
to be a simple class instead QObject
based.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am mostly arguing from a consistency standpoint here. This class is not performance-critical, so this bit of overhead rarely matters. We have a few classes which are not derived from QObject and for some it's vital, for most it's quite confusing. I've run into the situation often enough that I wanted to use a QMetaObject function on an instance of a class only to realise that it does not inherit from QObject. Adding QObject later is not as easy as you might think. Sometimes incompatible inheritance of usage patterns have evolved which makes it impossible to let a class inherit from QObject later without refactoring large parts of the code. For a Merger class, I could very well imagine use cases where you would want to use signals and slots later on. You are already using the translation functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, my experience is, that I don't want to use QObject
as long I don't have to. Since the change is trivial (for now), so I'll go along and inherit from QObject
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I won't fight to the death about this one, but I think consistency is a good thing here.
For objects which will exist many times in memory, I would agree with you, though.
src/proxy/NativeMessagingHost.cpp
Outdated
{ | ||
m_localSocket = new QLocalSocket(); | ||
m_localSocket->connectToServer(getLocalServerPath()); | ||
m_localSocket->setReadBufferSize(NATIVE_MSG_MAX_LENGTH); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove all these files from the diff which have no changes except minor formatting.
tests/TestMerge.cpp
Outdated
#include "core/Metadata.h" | ||
#include "crypto/Crypto.h" | ||
|
||
QTEST_GUILESS_MAIN(TestMerge) | ||
|
||
namespace | ||
{ | ||
TimeInfo modificationTime(TimeInfo timeInfo, int iYears, int iMonths, int iDays) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
years, months, days
tests/stub/TestClock.h
Outdated
|
||
namespace Test | ||
{ | ||
class Clock : public ::Clock |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just call this class ClockTest or TestClock? Needing to refer to the super class as ::Clock is confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since only Test::Clock
references ::Clock
it seems easier to write Test::Clock
(shows that the object should only be used in tests) instead of TestClock
/ClockTest
(may collide with a potential test for Clock
) or Test::TestClock
(redundant naming) in the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this naming. It's against all our conventions. Wrap it in an anonymous namespace if you are worried about name clashes, but ::Clock is not the way to go.
Thanks for your feedback. I'd like to add some more general thoughts:
|
I did not suggest adding a new transaction feature. I merely asked for starting one with We do not require you to run |
On the one side, it seems like I misunderstood "transaction" - on the other side, maybe it would be a good concept for the future (you already have some parts which do transaction the hard way like the edit widgets). Regarding formatting. I plead to format the code base consistently as soon as possible. I believe a lot of contributers are working on different projects, each with its own style guide. It is much easier to switch between projects and their code styles when the existing code serves as example than reading the style guide before or after writing code. This way one sees immediately when the new code deviates from the code style since it looks odd beside the existing code. |
Good point. We should think about that.
Well, we have and you see that your
True. We had to tell people to move |
The last commit should fixes the error for running I still have some doubts about the merge of deletedObjects, since they were not considered up to now:
Another point I want to mention is about the merge Currently, each and every group is able to differ in merge behavior from its parent and the rest of the database (at least as soon as the user is able to set this property). Depending, how it should be used, it may allow someone controlling the remote database to influence the local database in surprising behavior because relocated entries may use a different strategy than in their original location. |
DeletedObjects should never be overwritten or discarded. They are nothing but a reference to an entries/groups previous UUID (no data attached) and their only purpose is to allow bidirectional remote synchronisation and merging. We don't support remote merging atm, but KeePass does and if a merge strategy suddenly discards some DeletedObjects, it may result in weird and unintended effects when synchronizing the database with a WebDAV server etc. But DeletedObjects are also useful for local merging. |
I'll have a look at the KeePass code to make sure that we do have compatible behavior. |
I adjusted the merge strategies to include a history merge (I discarded the Finally, the state with a Update: Dominik Reichl intended the (for me surprising) behavior (see https://sourceforge.net/p/keepass/bugs/1752/). I'll change the tests accordingly to expect this behavior. |
@ckieschnick thanks for that PR! I did not have a chance to look at the changes in depth. Will this PR deprecate #1882? If so, do you think you can add the unit tests of #1882 in this PR? |
The new functionality of this PR does not check if a merge is required. The |
@ckieschnick Please implement all the operators in #1883 so we can close that PR in favor of this one |
@TheZ3ro The most of the requested comparison functions are implemented within the Concerning the implemented operators in #1883, I'd rather not implement the comparison using the |
I agree with @ckieschnick analysis here. |
So I think we can close #1883 since it's obsolete and conflict with this PR |
Sorry for the delay. I think this has matured quite a bit now and is ready to be merged into develop for real-world testing. I'll do the final review next week when I'm back home. In the meantime, could you please rebase the branch to the current develop HEAD? Thanks! |
@phoerious please complete your final review so this can be merged |
Seems that there are some files conflicting now |
* Create history-based merging that keeps older data in history instead of discarding or deleting it * Extract merge logic into the Merger class * Allows special merge behavior * Improve handling of deletion and changes on groups * Enable basic change tracking while merging * Prevent unintended timestamp changes while merging * Handle differences in timestamp precision * Introduce comparison operators to allow for more sophisticated comparisons (ignore special properties, ...) * Introduce Clock class to handle datetime across the app Merge Strategies: * Default (use inherited/fallback method) * Duplicate (duplicate conflicting nodes, apply all deletions) * KeepLocal (use local values, but apply all deletions) * KeepRemote (use remote values, but apply all deletions) * KeepNewer (merge history only) * Synchronize (merge history, newest value stays on top, apply all deletions)
6d9e673
to
2a90ddc
Compare
The changes requested were made
- New Database Wizard [#1952] - Advanced Search [#1797] - Automatic update checker [#2648] - KeeShare database synchronization [#2109, #1992, #2738, #2742, #2746, #2739] - Improve favicon fetching; transition to Duck-Duck-Go [#2795, #2011, #2439] - Remove KeePassHttp support [#1752] - CLI: output info to stderr for easier scripting [#2558] - CLI: Add --quiet option [#2507] - CLI: Add create command [#2540] - CLI: Add recursive listing of entries [#2345] - CLI: Fix stdin/stdout encoding on Windows [#2425] - SSH Agent: Support OpenSSH for Windows [#1994] - macOS: TouchID Quick Unlock [#1851] - macOS: Multiple improvements; include CLI in DMG [#2165, #2331, #2583] - Linux: Prevent Klipper from storing secrets in clipboard [#1969] - Linux: Use polling based file watching for NFS [#2171] - Linux: Enable use of browser plugin in Snap build [#2802] - TOTP QR Code Generator [#1167] - High-DPI Scaling for 4k screens [#2404] - Make keyboard shortcuts more consistent [#2431] - Warn user if deleting referenced entries [#1744] - Allow toolbar to be hidden and repositioned [#1819, #2357] - Increase max allowed database timeout to 12 hours [#2173] - Password generator uses existing password length by default [#2318] - Improve alert message box button labels [#2376] - Show message when a database merge makes no changes [#2551] - Browser Integration Enhancements [#1497, #2253, #1904, #2232, #1850, #2218, #2391, #2396, #2542, #2622, #2637, #2790] - Overall Code Improvements [#2316, #2284, #2351, #2402, #2410, #2419, #2422, #2443, #2491, #2506, #2610, #2667, #2709, #2731]
We're currently developing a sharing extension to KeePassXC which we like to contribute. During the development we discovered some problems which need to be addressed beforehand. These needed fixes and features are part of this pull request to allow you to review the new and changed behavior.
The changes are mainly concerned with ensuring to have consistent and expected modification timestamps and states since synchronized merge itself does not modify timestamps.
Description
We extracted the whole merge logic into a dedicated class
Merger
.Additionally, we needed some tooling, especially comparison methods:
Compare
encapsulates generic comparison methods and flagsEntry
,Group
,TimeInfo
, ... provideequals
.==
and!=
with additional options to allow for more sophisticated comparisons i.e. excluding location changes or excluding user statisticsFurthermore we introduced
Clock
which can be stubbed during tests withTestClock
to remove the dependency to the system time during tests.Motivation and context
There are currently a number of tickets related to sharing and synchronizing of databases which may profit using the changed implementation: supports #1152, #841, #637, and #90. Resolves #939 and resolves #818.
How has this been tested?
Tested using and extending the test suite and manually with code which will be part of a later contribution.
The code contains some
TODO
which point out some shortcomings, extension points or potentially surprising behavior.Types of changes
Checklist:
-DWITH_ASAN=ON
. [REQUIRED]EDIT @phoerious: Add "resolves" keywords in front of issue numbers