mirror of
https://github.com/silvanocerza/github-gitless-sync.git
synced 2026-07-22 12:10:28 +00:00
Compare commits
12 commits
1.0.7-beta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ebd201da9 | ||
|
|
ee9c2ee236 | ||
|
|
b6eb9ed62d | ||
|
|
59f951f2b8 | ||
|
|
c62afe938b | ||
|
|
69841aa574 | ||
|
|
345826443f | ||
|
|
e33de48075 | ||
|
|
b2f3db6050 | ||
|
|
be547d8691 | ||
|
|
4fa2526322 | ||
|
|
786cefaf4d |
15 changed files with 171 additions and 56 deletions
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
|
|
@ -20,14 +20,15 @@ jobs:
|
||||||
|
|
||||||
- name: Build plugin
|
- name: Build plugin
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install -g pnpm@latest-10
|
||||||
npm run build
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.ref_name }}" == *-beta ]]; then
|
if [[ "${{ github.ref_name }}" == *-beta* ]]; then
|
||||||
gh release create "${{ github.ref_name }}" \
|
gh release create "${{ github.ref_name }}" \
|
||||||
--title="${{ github.ref_name }}" \
|
--title="${{ github.ref_name }}" \
|
||||||
--prerelease \
|
--prerelease \
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -126,6 +126,16 @@ To work correctly this plugin uses a custom metadata file that is updated every
|
||||||
|
|
||||||
Other plugins don't know about that file, so if you sync with others too you risk losing data.
|
Other plugins don't know about that file, so if you sync with others too you risk losing data.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are obviously accepted.
|
||||||
|
|
||||||
|
Bug fixes are always welcome, new feature must be discussed first. Open a [discussion](https://github.com/silvanocerza/github-gitless-sync/discussions) and let's talk in that case.
|
||||||
|
|
||||||
|
Keep PRs as small as possible, I won't review PRs with hundreds of lines if it's mostly code.
|
||||||
|
|
||||||
|
Low quality or vibe coded PRs are not welcome. Put some effort in it please.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
The project is licensed under the [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
The project is licensed under the [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html) license.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "github-gitless-sync",
|
"id": "github-gitless-sync",
|
||||||
"name": "GitHub Gitless Sync",
|
"name": "GitHub Gitless Sync",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"minAppVersion": "1.7.7",
|
"minAppVersion": "1.7.7",
|
||||||
"description": "Sync a GitHub repository with vaults on different platforms without requiring git installation",
|
"description": "Sync a GitHub repository with vaults on different platforms without requiring git installation",
|
||||||
"author": "Silvano Cerza",
|
"author": "Silvano Cerza",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "github-gitless-sync",
|
"name": "github-gitless-sync",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"description": "Sync a GitHub repository with vaults on different platforms without requiring git installation",
|
"description": "Sync a GitHub repository with vaults on different platforms without requiring git installation",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Vault, TAbstractFile, TFolder } from "obsidian";
|
import { Vault, TAbstractFile, TFolder } from "obsidian";
|
||||||
import MetadataStore, { MANIFEST_FILE_NAME } from "./metadata-store";
|
import MetadataStore, { MANIFEST_FILE_NAME } from "./metadata-store";
|
||||||
import { GitHubSyncSettings } from "./settings/settings";
|
import { GitHubSyncSettings } from "./settings/settings";
|
||||||
import Logger from "./logger";
|
import Logger, { LOG_FILE_NAME } from "./logger";
|
||||||
import GitHubSyncPlugin from "./main";
|
import GitHubSyncPlugin from "./main";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -145,6 +145,9 @@ export default class EventsListener {
|
||||||
) {
|
) {
|
||||||
// Obsidian recommends not syncing the workspace files
|
// Obsidian recommends not syncing the workspace files
|
||||||
return false;
|
return false;
|
||||||
|
} else if (filePath === `${this.vault.configDir}/${LOG_FILE_NAME}`) {
|
||||||
|
// Don't sync the log file, doesn't make sense
|
||||||
|
return false;
|
||||||
} else if (
|
} else if (
|
||||||
this.settings.syncConfigDir &&
|
this.settings.syncConfigDir &&
|
||||||
filePath.startsWith(this.vault.configDir)
|
filePath.startsWith(this.vault.configDir)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Vault, normalizePath } from "obsidian";
|
import { Vault, normalizePath } from "obsidian";
|
||||||
|
|
||||||
const LOG_FILE_NAME = "github-sync.log" as const;
|
export const LOG_FILE_NAME = "github-sync.log" as const;
|
||||||
|
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
private logFile: string;
|
private logFile: string;
|
||||||
|
|
|
||||||
10
src/main.ts
10
src/main.ts
|
|
@ -152,9 +152,19 @@ export default class GitHubSyncPlugin extends Plugin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.settings.firstSync) {
|
if (this.settings.firstSync) {
|
||||||
|
const notice = new Notice("Syncing...");
|
||||||
|
try {
|
||||||
await this.syncManager.firstSync();
|
await this.syncManager.firstSync();
|
||||||
this.settings.firstSync = false;
|
this.settings.firstSync = false;
|
||||||
this.saveSettings();
|
this.saveSettings();
|
||||||
|
// Shown only if sync doesn't fail
|
||||||
|
new Notice("Sync successful", 5000);
|
||||||
|
} catch (err) {
|
||||||
|
// Show the error to the user, it's not automatically dismissed to make sure
|
||||||
|
// the user sees it.
|
||||||
|
new Notice(`Error syncing. ${err}`);
|
||||||
|
}
|
||||||
|
notice.hide();
|
||||||
} else {
|
} else {
|
||||||
await this.syncManager.sync();
|
await this.syncManager.sync();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import MetadataStore, {
|
||||||
} from "./metadata-store";
|
} from "./metadata-store";
|
||||||
import EventsListener from "./events-listener";
|
import EventsListener from "./events-listener";
|
||||||
import { GitHubSyncSettings } from "./settings/settings";
|
import { GitHubSyncSettings } from "./settings/settings";
|
||||||
import Logger from "./logger";
|
import Logger, { LOG_FILE_NAME } from "./logger";
|
||||||
import { decodeBase64String, hasTextExtension } from "./utils";
|
import { decodeBase64String, hasTextExtension } from "./utils";
|
||||||
import GitHubSyncPlugin from "./main";
|
import GitHubSyncPlugin from "./main";
|
||||||
import { BlobReader, Entry, Uint8ArrayWriter, ZipReader } from "@zip.js/zip.js";
|
import { BlobReader, Entry, Uint8ArrayWriter, ZipReader } from "@zip.js/zip.js";
|
||||||
|
|
@ -95,19 +95,14 @@ export default class SyncManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notice = new Notice("Syncing...");
|
|
||||||
this.syncing = true;
|
this.syncing = true;
|
||||||
try {
|
try {
|
||||||
await this.firstSyncImpl();
|
await this.firstSyncImpl();
|
||||||
// Shown only if sync doesn't fail
|
|
||||||
new Notice("Sync successful", 5000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Show the error to the user, it's not automatically dismissed to make sure
|
this.syncing = false;
|
||||||
// the user sees it.
|
throw err;
|
||||||
new Notice(`Error syncing. ${err}`);
|
|
||||||
}
|
}
|
||||||
this.syncing = false;
|
this.syncing = false;
|
||||||
notice.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async firstSyncImpl() {
|
private async firstSyncImpl() {
|
||||||
|
|
@ -142,9 +137,12 @@ export default class SyncManager {
|
||||||
// API doesn't let us create a new tree when the repo is empty.
|
// API doesn't let us create a new tree when the repo is empty.
|
||||||
// So we create a the manifest file as the first commit, since we're going
|
// So we create a the manifest file as the first commit, since we're going
|
||||||
// to create that in any case right after this.
|
// to create that in any case right after this.
|
||||||
|
const buffer = await this.vault.adapter.readBinary(
|
||||||
|
normalizePath(`${this.vault.configDir}/${MANIFEST_FILE_NAME}`),
|
||||||
|
);
|
||||||
await this.client.createFile({
|
await this.client.createFile({
|
||||||
path: `${this.vault.configDir}/${MANIFEST_FILE_NAME}`,
|
path: `${this.vault.configDir}/${MANIFEST_FILE_NAME}`,
|
||||||
content: "",
|
content: arrayBufferToBase64(buffer),
|
||||||
message: "First sync",
|
message: "First sync",
|
||||||
retry: true,
|
retry: true,
|
||||||
});
|
});
|
||||||
|
|
@ -235,6 +233,14 @@ export default class SyncManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetPath === `${this.vault.configDir}/${LOG_FILE_NAME}`) {
|
||||||
|
// We don't want to download the log file if the user synced it in the past.
|
||||||
|
// This is necessary because in the past we forgot to ignore the log file
|
||||||
|
// from syncing if the user enabled configs sync.
|
||||||
|
// To avoid downloading it we ignore it if still present in the remote repo.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (targetPath.split("/").last()?.startsWith(".")) {
|
if (targetPath.split("/").last()?.startsWith(".")) {
|
||||||
// We must skip hidden files as that creates issues with syncing.
|
// We must skip hidden files as that creates issues with syncing.
|
||||||
// This is fine as users can't edit hidden files in Obsidian anyway.
|
// This is fine as users can't edit hidden files in Obsidian anyway.
|
||||||
|
|
@ -259,6 +265,14 @@ export default class SyncManager {
|
||||||
await this.logger.info("Written file", {
|
await this.logger.info("Written file", {
|
||||||
normalizedPath,
|
normalizedPath,
|
||||||
});
|
});
|
||||||
|
this.metadataStore.data.files[normalizedPath] = {
|
||||||
|
path: normalizedPath,
|
||||||
|
sha: files[normalizedPath].sha,
|
||||||
|
dirty: false,
|
||||||
|
justDownloaded: true,
|
||||||
|
lastModified: Date.now(),
|
||||||
|
};
|
||||||
|
await this.metadataStore.save();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -280,18 +294,33 @@ export default class SyncManager {
|
||||||
);
|
);
|
||||||
// Add files that are in the manifest but not in the tree.
|
// Add files that are in the manifest but not in the tree.
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(this.metadataStore.data.files).map(
|
Object.keys(this.metadataStore.data.files)
|
||||||
async (filePath: string) => {
|
.filter((filePath: string) => {
|
||||||
|
return !Object.keys(files).contains(filePath);
|
||||||
|
})
|
||||||
|
.map(async (filePath: string) => {
|
||||||
const normalizedPath = normalizePath(filePath);
|
const normalizedPath = normalizePath(filePath);
|
||||||
const content = await this.vault.adapter.read(normalizedPath);
|
// We need to check whether the file is a text file or not before
|
||||||
|
// reading it here because trying to read a binary file as text fails
|
||||||
|
// on iOS, and probably on other mobile devices too, so we read the file
|
||||||
|
// content only if we're sure it contains text only.
|
||||||
|
//
|
||||||
|
// It's fine not reading the binary file in here and just setting some bogus
|
||||||
|
// content because when committing the sync we're going to read the binary
|
||||||
|
// file and upload its blob if it needs to be synced. The important thing is
|
||||||
|
// that some content is set so we know the file changed locally and needs to be
|
||||||
|
// uploaded.
|
||||||
|
let content = "binaryfile";
|
||||||
|
if (hasTextExtension(normalizedPath)) {
|
||||||
|
content = await this.vault.adapter.read(normalizedPath);
|
||||||
|
}
|
||||||
newTreeFiles[filePath] = {
|
newTreeFiles[filePath] = {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: "100644",
|
||||||
type: "blob",
|
type: "blob",
|
||||||
content: content,
|
content,
|
||||||
};
|
};
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
await this.commitSync(newTreeFiles, treeSha);
|
await this.commitSync(newTreeFiles, treeSha);
|
||||||
}
|
}
|
||||||
|
|
@ -324,18 +353,36 @@ export default class SyncManager {
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.keys(this.metadataStore.data.files).map(
|
Object.keys(this.metadataStore.data.files)
|
||||||
async (filePath: string) => {
|
.filter((filePath: string) => {
|
||||||
|
// We should not try to sync deleted files, this can happen when
|
||||||
|
// the user renames or deletes files after enabling the plugin but
|
||||||
|
// before syncing for the first time
|
||||||
|
return !this.metadataStore.data.files[filePath].deleted;
|
||||||
|
})
|
||||||
|
.map(async (filePath: string) => {
|
||||||
const normalizedPath = normalizePath(filePath);
|
const normalizedPath = normalizePath(filePath);
|
||||||
const content = await this.vault.adapter.read(normalizedPath);
|
// We need to check whether the file is a text file or not before
|
||||||
|
// reading it here because trying to read a binary file as text fails
|
||||||
|
// on iOS, and probably on other mobile devices too, so we read the file
|
||||||
|
// content only if we're sure it contains text only.
|
||||||
|
//
|
||||||
|
// It's fine not reading the binary file in here and just setting some bogus
|
||||||
|
// content because when committing the sync we're going to read the binary
|
||||||
|
// file and upload its blob if it needs to be synced. The important thing is
|
||||||
|
// that some content is set so we know the file changed locally and needs to be
|
||||||
|
// uploaded.
|
||||||
|
let content = "binaryfile";
|
||||||
|
if (hasTextExtension(normalizedPath)) {
|
||||||
|
content = await this.vault.adapter.read(normalizedPath);
|
||||||
|
}
|
||||||
newTreeFiles[filePath] = {
|
newTreeFiles[filePath] = {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: "100644",
|
||||||
type: "blob",
|
type: "blob",
|
||||||
content: content,
|
content,
|
||||||
};
|
};
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
await this.commitSync(newTreeFiles, treeSha);
|
await this.commitSync(newTreeFiles, treeSha);
|
||||||
}
|
}
|
||||||
|
|
@ -378,6 +425,16 @@ export default class SyncManager {
|
||||||
throw new Error("Remote manifest is missing");
|
throw new Error("Remote manifest is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(files).contains(`${this.vault.configDir}/${LOG_FILE_NAME}`)
|
||||||
|
) {
|
||||||
|
// We don't want to download the log file if the user synced it in the past.
|
||||||
|
// This is necessary because in the past we forgot to ignore the log file
|
||||||
|
// from syncing if the user enabled configs sync.
|
||||||
|
// To avoid downloading it we delete it if still around.
|
||||||
|
delete files[`${this.vault.configDir}/${LOG_FILE_NAME}`];
|
||||||
|
}
|
||||||
|
|
||||||
const blob = await this.client.getBlob({ sha: manifest.sha });
|
const blob = await this.client.getBlob({ sha: manifest.sha });
|
||||||
const remoteMetadata: Metadata = JSON.parse(
|
const remoteMetadata: Metadata = JSON.parse(
|
||||||
decodeBase64String(blob.content),
|
decodeBase64String(blob.content),
|
||||||
|
|
@ -643,16 +700,19 @@ export default class SyncManager {
|
||||||
type: "delete_local",
|
type: "delete_local",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
localFile.lastModified > (remoteFile.deletedAt as number)
|
localFile.lastModified > (remoteFile.deletedAt as number)
|
||||||
) {
|
) {
|
||||||
actions.push({ type: "upload", filePath: filePath });
|
actions.push({ type: "upload", filePath: filePath });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!remoteFile.deleted && localFile.deleted) {
|
if (!remoteFile.deleted && localFile.deleted) {
|
||||||
if (remoteFile.lastModified > (localFile.deletedAt as number)) {
|
if (remoteFile.lastModified > (localFile.deletedAt as number)) {
|
||||||
actions.push({ type: "download", filePath: filePath });
|
actions.push({ type: "download", filePath: filePath });
|
||||||
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
(localFile.deletedAt as number) > remoteFile.lastModified
|
(localFile.deletedAt as number) > remoteFile.lastModified
|
||||||
) {
|
) {
|
||||||
|
|
@ -660,6 +720,7 @@ export default class SyncManager {
|
||||||
type: "delete_remote",
|
type: "delete_remote",
|
||||||
filePath: filePath,
|
filePath: filePath,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -667,8 +728,10 @@ export default class SyncManager {
|
||||||
// Conflicts are already filtered out so we can make this decision easily
|
// Conflicts are already filtered out so we can make this decision easily
|
||||||
if (localSHA !== localFile.sha) {
|
if (localSHA !== localFile.sha) {
|
||||||
actions.push({ type: "upload", filePath: filePath });
|
actions.push({ type: "upload", filePath: filePath });
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
actions.push({ type: "download", filePath: filePath });
|
actions.push({ type: "download", filePath: filePath });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -727,9 +790,13 @@ export default class SyncManager {
|
||||||
* This is the same identical algoritm used by git to calculate
|
* This is the same identical algoritm used by git to calculate
|
||||||
* a blob's SHA.
|
* a blob's SHA.
|
||||||
* @param filePath normalized path to file
|
* @param filePath normalized path to file
|
||||||
* @returns String containing the file SHA1
|
* @returns String containing the file SHA1 or null in case the file doesn't exist
|
||||||
*/
|
*/
|
||||||
async calculateSHA(filePath: string): Promise<string> {
|
async calculateSHA(filePath: string): Promise<string | null> {
|
||||||
|
if (!(await this.vault.adapter.exists(filePath))) {
|
||||||
|
// The file doesn't exist, can't calculate any SHA
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const contentBuffer = await this.vault.adapter.readBinary(filePath);
|
const contentBuffer = await this.vault.adapter.readBinary(filePath);
|
||||||
const contentBytes = new Uint8Array(contentBuffer);
|
const contentBytes = new Uint8Array(contentBuffer);
|
||||||
const header = new TextEncoder().encode(`blob ${contentBytes.length}\0`);
|
const header = new TextEncoder().encode(`blob ${contentBytes.length}\0`);
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,6 @@ const ActionsGutter: React.FC<ActionsGutterProps> = ({
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
overflow: "hidden",
|
|
||||||
position: "relative",
|
position: "relative",
|
||||||
backgroundColor: "var(--background-secondary)",
|
backgroundColor: "var(--background-secondary)",
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,11 @@ const DiffView: React.FC<DiffViewProps> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ flex: 1, overflow: "hidden" }}>
|
<div style={{ flex: 1 }}>
|
||||||
<EditorPane
|
<EditorPane
|
||||||
content={remoteText}
|
content={remoteText}
|
||||||
highlightPluginSpec={{
|
highlightPluginSpec={{
|
||||||
|
|
@ -142,7 +140,7 @@ const DiffView: React.FC<DiffViewProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, overflow: "hidden" }}>
|
<div style={{ flex: 1 }}>
|
||||||
<EditorPane
|
<EditorPane
|
||||||
content={localText}
|
content={localText}
|
||||||
highlightPluginSpec={{
|
highlightPluginSpec={{
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,9 @@ const SplitView = ({
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -88,6 +88,12 @@ const SplitView = ({
|
||||||
currentFile={currentFile?.filePath || ""}
|
currentFile={currentFile?.filePath || ""}
|
||||||
setCurrentFileIndex={setCurrentFileIndex}
|
setCurrentFileIndex={setCurrentFileIndex}
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflow: "auto",
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DiffView
|
<DiffView
|
||||||
remoteText={currentFile?.remoteContent || ""}
|
remoteText={currentFile?.remoteContent || ""}
|
||||||
localText={currentFile?.localContent || ""}
|
localText={currentFile?.localContent || ""}
|
||||||
|
|
@ -103,6 +109,7 @@ const SplitView = ({
|
||||||
}}
|
}}
|
||||||
onConflictResolved={onConflictResolved}
|
onConflictResolved={onConflictResolved}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,15 @@ const DiffView: React.FC<DiffViewProps> = ({
|
||||||
}, [initialRemoteText, initialLocalText]);
|
}, [initialRemoteText, initialLocalText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%", height: "100%", overflow: "hidden" }}>
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
value={doc}
|
value={doc}
|
||||||
height="100%"
|
height="100%"
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ const UnifiedView = ({
|
||||||
paddingTop: "var(--size-4-4)",
|
paddingTop: "var(--size-4-4)",
|
||||||
paddingBottom: "var(--size-4-4)",
|
paddingBottom: "var(--size-4-4)",
|
||||||
borderBottom: "1px solid var(--background-modifier-border)",
|
borderBottom: "1px solid var(--background-modifier-border)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -67,7 +69,14 @@ const UnifiedView = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<div>
|
<div
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,6 @@
|
||||||
|
|
||||||
.padless-conflicts-view-container {
|
.padless-conflicts-view-container {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,6 @@
|
||||||
"1.0.3": "1.7.7",
|
"1.0.3": "1.7.7",
|
||||||
"1.0.4": "1.7.7",
|
"1.0.4": "1.7.7",
|
||||||
"1.0.5": "1.7.7",
|
"1.0.5": "1.7.7",
|
||||||
"1.0.6": "1.7.7"
|
"1.0.6": "1.7.7",
|
||||||
|
"1.0.7": "1.7.7"
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue