mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 23:00:30 +00:00
0a616fba6b
* VCS proof of concept underway! * Stuff * Some things * Replace deprecated Electron makeSingleInstance * Rename `window` variables so not to be confused with window object * Don't unnecessarily update request when URL does not change * Regenerate package-lock * Fix tests + ESLint * Publish - insomnia-app@1.0.49 - insomnia-cookies@0.0.12 - insomnia-httpsnippet@1.16.18 - insomnia-importers@2.0.13 - insomnia-libcurl@0.0.23 - insomnia-prettify@0.1.7 - insomnia-url@0.1.6 - insomnia-xpath@1.0.9 - insomnia-plugin-base64@1.0.6 - insomnia-plugin-cookie-jar@1.0.8 - insomnia-plugin-core-themes@1.0.5 - insomnia-plugin-default-headers@1.1.9 - insomnia-plugin-file@1.0.7 - insomnia-plugin-hash@1.0.7 - insomnia-plugin-jsonpath@1.0.12 - insomnia-plugin-now@1.0.11 - insomnia-plugin-os@1.0.13 - insomnia-plugin-prompt@1.1.9 - insomnia-plugin-request@1.0.18 - insomnia-plugin-response@1.0.16 - insomnia-plugin-uuid@1.0.10 * Broken but w/e * Some tweaks * Big refactor. Create local snapshots and push done * POC merging and a lot of improvements * Lots of work done on initial UI/UX * Fix old tests * Atomic writes and size-based batches * Update StageEntry definition once again to be better * Factor out GraphQL query logic * Merge algorithm, history modal, other minor things * Fix test * Merge, checkout, revert w/ user changes now work * Force UI to refresh when switching branches changes active request * Rough draft pull() and some cleanup * E2EE stuff and some refactoring * Add ability to share project with team and fixed tests * VCS now created in root component and better remote project handling * Remove unused definition * Publish - insomnia-account@0.0.2 - insomnia-app@1.1.1 - insomnia-cookies@0.0.14 - insomnia-httpsnippet@1.16.20 - insomnia-importers@2.0.15 - insomnia-libcurl@0.0.25 - insomnia-prettify@0.1.9 - insomnia-sync@0.0.2 - insomnia-url@0.1.8 - insomnia-xpath@1.0.11 - insomnia-plugin-base64@1.0.8 - insomnia-plugin-cookie-jar@1.0.10 - insomnia-plugin-core-themes@1.0.7 - insomnia-plugin-file@1.0.9 - insomnia-plugin-hash@1.0.9 - insomnia-plugin-jsonpath@1.0.14 - insomnia-plugin-now@1.0.13 - insomnia-plugin-os@1.0.15 - insomnia-plugin-prompt@1.1.11 - insomnia-plugin-request@1.0.20 - insomnia-plugin-response@1.0.18 - insomnia-plugin-uuid@1.0.12 * Move some deps around * Fix Flow errors * Update package.json * Fix eslint errors * Fix tests * Update deps * bootstrap insomnia-sync * TRy fixing appveyor * Try something else * Bump lerna * try powershell * Try again * Fix imports * Fixed errors * sync types refactor * Show remote projects in workspace dropdown * Improved pulling of non-local workspaces * Loading indicators and some tweaks * Clean up sync staging modal * Some sync improvements: - No longer store stage - Upgrade Electron - Sync UI/UX improvements * Fix snyc tests * Upgraded deps and hot loader tweaks (it's broken for some reason) * Fix tests * Branches dialog, network refactoring, some tweaks * Fixed merging when other branch is empty * A bunch of small fixes from real testing * Fixed pull merge logic * Fix tests * Some bug fixes * A few small tweaks * Conflict resolution and other improvements * Fix tests * Add revert changes * Deal with duplicate projects per workspace * Some tweaks and accessibility improvements * Tooltip accessibility * Fix API endpoint * Fix tests * Remove jest dep from insomnia-importers
479 lines
11 KiB
JavaScript
479 lines
11 KiB
JavaScript
// @flow
|
|
import clone from 'clone';
|
|
import crypto from 'crypto';
|
|
import { deterministicStringify } from '../lib/deterministicStringify';
|
|
import type {
|
|
Branch,
|
|
DocumentKey,
|
|
MergeConflict,
|
|
Snapshot,
|
|
SnapshotState,
|
|
SnapshotStateEntry,
|
|
SnapshotStateMap,
|
|
StageEntry,
|
|
StatusCandidate,
|
|
StatusCandidateMap,
|
|
} from '../types';
|
|
|
|
export function generateSnapshotStateMap(snapshot: Snapshot | null): SnapshotStateMap {
|
|
if (!snapshot) {
|
|
return {};
|
|
}
|
|
|
|
return generateStateMap(snapshot.state);
|
|
}
|
|
|
|
export function generateStateMap(state: SnapshotState | null): SnapshotStateMap {
|
|
if (!state) {
|
|
return {};
|
|
}
|
|
|
|
const map = {};
|
|
for (const entry of state) {
|
|
map[entry.key] = entry;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
export function generateCandidateMap(candidates: Array<StatusCandidate>): StatusCandidateMap {
|
|
const map = {};
|
|
for (const candidate of candidates) {
|
|
map[candidate.key] = candidate;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
export function combinedMapKeys<T: SnapshotStateMap | StatusCandidateMap>(
|
|
...maps: Array<T>
|
|
): Array<DocumentKey> {
|
|
const keyMap = {};
|
|
for (const map of maps) {
|
|
for (const key of Object.keys(map)) {
|
|
keyMap[key] = true;
|
|
}
|
|
}
|
|
|
|
return Object.keys(keyMap);
|
|
}
|
|
|
|
export function threeWayMerge(
|
|
root: SnapshotState,
|
|
trunk: SnapshotState,
|
|
other: SnapshotState,
|
|
): { state: SnapshotState, conflicts: Array<MergeConflict> } {
|
|
const stateRoot = generateStateMap(root);
|
|
const stateTrunk = generateStateMap(trunk);
|
|
const stateOther = generateStateMap(other);
|
|
const allKeys = combinedMapKeys(stateRoot, stateTrunk, stateOther);
|
|
|
|
const newState: SnapshotState = [];
|
|
const conflicts: Array<MergeConflict> = [];
|
|
|
|
for (const key of allKeys) {
|
|
const root = stateRoot[key] || null;
|
|
const trunk = stateTrunk[key] || null;
|
|
const other = stateOther[key] || null;
|
|
|
|
// This condition tree checks every possible case that root, trunk, other can be in
|
|
// separately. YES, it could be simplified but we want to expand every case to make
|
|
// it as bulletproof and readable as possible.
|
|
//
|
|
// Here are the possible combinations:
|
|
// root => [ exists, missing ]
|
|
// trunk => [ exists, missing, modified ]
|
|
// other => [ exists, missing, modified ]
|
|
//
|
|
// Therefore, the total number of cases is equal to 2 * 3! = 12
|
|
|
|
// ~~~~~~~~~~ //
|
|
// Unmodified //
|
|
// ~~~~~~~~~~ //
|
|
|
|
// (1/12)
|
|
// Unmodified
|
|
if (root && trunk && other && root.blob === trunk.blob && root.blob === other.blob) {
|
|
newState.push(trunk);
|
|
continue;
|
|
}
|
|
|
|
// ~~~~~~~~~ //
|
|
// Deletions //
|
|
// ~~~~~~~~~ //
|
|
|
|
// (2/12)
|
|
// Deleted in both
|
|
if (root && trunk === null && other === null) {
|
|
continue;
|
|
}
|
|
|
|
// (3/12)
|
|
// Deleted in trunk
|
|
if (root && trunk === null && other && other.blob === root.blob) {
|
|
continue;
|
|
}
|
|
|
|
// (4/12)
|
|
// Deleted in other
|
|
if (root && trunk && other === null && trunk.blob === root.blob) {
|
|
continue;
|
|
}
|
|
|
|
// ~~~~~~~~~ //
|
|
// Additions //
|
|
// ~~~~~~~~~ //
|
|
|
|
// (5/12)
|
|
// Added in both
|
|
if (root === null && trunk && other) {
|
|
if (trunk.blob !== other.blob) {
|
|
conflicts.push({
|
|
key,
|
|
name: other.name,
|
|
message: 'both added',
|
|
mineBlob: trunk.blob,
|
|
theirsBlob: other.blob,
|
|
choose: other.blob,
|
|
});
|
|
}
|
|
|
|
newState.push(trunk || other);
|
|
continue;
|
|
}
|
|
|
|
// (6/12)
|
|
// Added in trunk
|
|
if (root === null && trunk && other === null) {
|
|
newState.push(trunk);
|
|
continue;
|
|
}
|
|
|
|
// (7/12)
|
|
// Added in other
|
|
if (root === null && trunk === null && other) {
|
|
newState.push(other);
|
|
continue;
|
|
}
|
|
|
|
// ~~~~~~~~~~~~~ //
|
|
// Modifications //
|
|
// ~~~~~~~~~~~~~ //
|
|
|
|
// (8/12)
|
|
// Modified in both
|
|
if (root && trunk && other && root.blob !== trunk.blob && root.blob !== other.blob) {
|
|
if (trunk.blob !== other.blob) {
|
|
conflicts.push({
|
|
key,
|
|
name: other.name,
|
|
message: 'both modified',
|
|
mineBlob: trunk.blob,
|
|
theirsBlob: other.blob,
|
|
choose: other.blob,
|
|
});
|
|
}
|
|
|
|
newState.push(trunk);
|
|
continue;
|
|
}
|
|
|
|
// (9/12)
|
|
// Modified in trunk
|
|
if (root && trunk && other && root.blob !== trunk.blob && root.blob === other.blob) {
|
|
newState.push(trunk);
|
|
continue;
|
|
}
|
|
|
|
// (10/12)
|
|
// Modified in other
|
|
if (root && trunk && other && root.blob === trunk.blob && root.blob !== other.blob) {
|
|
newState.push(other);
|
|
continue;
|
|
}
|
|
|
|
// ~~~~~~ //
|
|
// Combos //
|
|
// ~~~~~~ //
|
|
|
|
// (11/12)
|
|
// Deleted in trunk and modified in other
|
|
if (root && trunk === null && other && other.blob !== root.blob) {
|
|
conflicts.push({
|
|
key,
|
|
name: other.name,
|
|
message: 'you deleted and they modified',
|
|
mineBlob: null,
|
|
theirsBlob: other.blob,
|
|
choose: other.blob,
|
|
});
|
|
newState.push(other);
|
|
continue;
|
|
}
|
|
|
|
// (12/12)
|
|
// Deleted in other and modified in trunk
|
|
if (root && trunk && other === null && trunk.blob !== root.blob) {
|
|
conflicts.push({
|
|
key,
|
|
name: root.name,
|
|
message: 'they deleted and you modified',
|
|
mineBlob: trunk.blob,
|
|
theirsBlob: null,
|
|
choose: trunk.blob,
|
|
});
|
|
newState.push(trunk);
|
|
continue;
|
|
}
|
|
|
|
// This should never actually happen, but let's error just to be safe
|
|
throw new Error('3-way merge hit impossible state');
|
|
}
|
|
|
|
return {
|
|
state: newState,
|
|
conflicts: conflicts,
|
|
};
|
|
}
|
|
|
|
export function compareBranches(
|
|
a: Branch | null,
|
|
b: Branch | null,
|
|
): { ahead: number, behind: number } {
|
|
const snapshotsA = a ? a.snapshots : [];
|
|
const snapshotsB = b ? b.snapshots : [];
|
|
const latestA = snapshotsA[snapshotsA.length - 1] || null;
|
|
const latestB = snapshotsB[snapshotsB.length - 1] || null;
|
|
|
|
const result = {
|
|
ahead: 0,
|
|
behind: 0,
|
|
};
|
|
|
|
if (latestA === latestB) {
|
|
return result;
|
|
}
|
|
|
|
if (latestA === null) {
|
|
result.behind = snapshotsB.length;
|
|
return result;
|
|
}
|
|
|
|
if (latestB === null) {
|
|
result.ahead = snapshotsA.length;
|
|
return result;
|
|
}
|
|
|
|
const root = getRootSnapshot(a, b);
|
|
if (root === null) {
|
|
return result;
|
|
}
|
|
|
|
const indexOfRootInA = snapshotsA.indexOf(root);
|
|
const indexOfRootInB = snapshotsB.indexOf(root);
|
|
|
|
result.ahead = snapshotsA.length - indexOfRootInA - 1;
|
|
result.behind = snapshotsB.length - indexOfRootInB - 1;
|
|
|
|
return result;
|
|
}
|
|
|
|
export function stateDelta(
|
|
base: SnapshotState,
|
|
desired: SnapshotState,
|
|
): {
|
|
add: Array<SnapshotStateEntry>,
|
|
update: Array<SnapshotStateEntry>,
|
|
remove: Array<SnapshotStateEntry>,
|
|
} {
|
|
const result = {
|
|
add: [],
|
|
update: [],
|
|
remove: [],
|
|
};
|
|
|
|
const stateMapStart = generateStateMap(base);
|
|
const stateMapFinish = generateStateMap(desired);
|
|
|
|
for (const key of combinedMapKeys(stateMapStart, stateMapFinish)) {
|
|
const start = stateMapStart[key];
|
|
const finish = stateMapFinish[key];
|
|
|
|
if (!start && finish) {
|
|
result.add.push(finish);
|
|
continue;
|
|
}
|
|
|
|
if (start && !finish) {
|
|
result.remove.push(start);
|
|
continue;
|
|
}
|
|
|
|
if (start && finish && start.blob !== finish.blob) {
|
|
result.update.push(finish);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function getStagable(
|
|
state: SnapshotState,
|
|
candidates: Array<StatusCandidate>,
|
|
): Array<StageEntry> {
|
|
const stagable: Array<StageEntry> = [];
|
|
|
|
const stateMap = generateStateMap(state);
|
|
const candidateMap = generateCandidateMap(candidates);
|
|
for (const key of combinedMapKeys(stateMap, candidateMap)) {
|
|
const entry = stateMap[key];
|
|
const candidate = candidateMap[key];
|
|
|
|
if (!entry && candidate) {
|
|
const { name, document } = candidate;
|
|
const { hash: blobId, content: blobContent } = hashDocument(document);
|
|
stagable.push({ key, name, blobId, blobContent, added: true });
|
|
continue;
|
|
}
|
|
|
|
if (entry && !candidate) {
|
|
const { name, blob: blobId } = entry;
|
|
stagable.push({ key, name, blobId, deleted: true });
|
|
continue;
|
|
}
|
|
|
|
if (entry && candidate) {
|
|
const { document, name } = candidate;
|
|
const { hash: blobId, content: blobContent } = hashDocument(document);
|
|
if (entry.blob !== blobId) {
|
|
stagable.push({ key, name, blobId, blobContent, modified: true });
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return stagable;
|
|
}
|
|
|
|
export function getRootSnapshot(a: Branch | null, b: Branch | null): string | null {
|
|
const snapshotsA = a ? a.snapshots : [];
|
|
const snapshotsB = b ? b.snapshots : [];
|
|
|
|
let rootSnapshotId = '';
|
|
for (let ai = snapshotsA.length - 1; ai >= 0; ai--) {
|
|
for (let bi = snapshotsB.length - 1; bi >= 0; bi--) {
|
|
if (snapshotsA[ai] === snapshotsB[bi]) {
|
|
return snapshotsA[ai];
|
|
}
|
|
}
|
|
}
|
|
return rootSnapshotId || null;
|
|
}
|
|
|
|
export function preMergeCheck(
|
|
trunkState: SnapshotState,
|
|
otherState: SnapshotState,
|
|
candidates: Array<StatusCandidate>,
|
|
): {
|
|
conflicts: Array<StatusCandidate>,
|
|
dirty: Array<StatusCandidate>,
|
|
} {
|
|
const result = {
|
|
conflicts: [],
|
|
dirty: [],
|
|
};
|
|
|
|
const trunkMap = generateStateMap(trunkState);
|
|
const otherMap = generateStateMap(otherState);
|
|
|
|
for (const candidate of candidates) {
|
|
const { key } = candidate;
|
|
const trunk = trunkMap[key];
|
|
const other = otherMap[key];
|
|
|
|
// Candidate is not in trunk or other (not yet in version control)
|
|
if (!trunk && !other) {
|
|
result.dirty.push(candidate);
|
|
continue;
|
|
}
|
|
|
|
const { hash: blobId } = hashDocument(candidate.document);
|
|
|
|
// Candidate is same as trunk (unchanged) so anything goes
|
|
if (trunk && trunk.blob === blobId) {
|
|
continue;
|
|
}
|
|
|
|
// Candidate is the same as trunk (nothing to do)
|
|
if (trunk && blobId === trunk.blob) {
|
|
continue;
|
|
}
|
|
|
|
// Candidate is the same as other (would update to same value)
|
|
if (other && blobId === other.blob) {
|
|
continue;
|
|
}
|
|
|
|
// Candidate is different but trunk and other are the same (preserve safe change)
|
|
if (
|
|
other &&
|
|
trunk &&
|
|
other.blob === trunk.blob &&
|
|
blobId !== other.blob &&
|
|
blobId !== trunk.blob
|
|
) {
|
|
result.dirty.push(candidate);
|
|
continue;
|
|
}
|
|
|
|
// All other cases result in conflict
|
|
result.conflicts.push(candidate);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function hashDocument(doc: Object): { content: string, hash: string } {
|
|
if (!doc) {
|
|
throw new Error('Cannot hash undefined value');
|
|
}
|
|
|
|
// Remove fields we don't care about for sync purposes
|
|
const newDoc = clone(doc);
|
|
delete newDoc.modified;
|
|
|
|
const content = deterministicStringify(newDoc);
|
|
const hash = crypto
|
|
.createHash('sha1')
|
|
.update(content)
|
|
.digest('hex');
|
|
|
|
return { hash, content };
|
|
}
|
|
|
|
export function updateStateWithConflictResolutions(
|
|
state: SnapshotState,
|
|
conflicts: Array<MergeConflict>,
|
|
): SnapshotState {
|
|
const newStateMap = generateStateMap(state);
|
|
for (const { choose, key, name } of conflicts) {
|
|
const stateEntry = state.find(e => e.key === key);
|
|
|
|
// Not in the state, but we choose the conflict
|
|
if (!stateEntry && choose !== null) {
|
|
newStateMap[key] = { key, name, blob: choose };
|
|
continue;
|
|
}
|
|
|
|
// Add the conflict
|
|
if (choose !== null) {
|
|
newStateMap[key] = { ...stateEntry, blob: choose };
|
|
continue;
|
|
}
|
|
|
|
// Chose to delete it, so don't add to state
|
|
delete newStateMap[key];
|
|
}
|
|
|
|
return Object.keys(newStateMap).map(k => newStateMap[k]);
|
|
}
|