diff --git a/packages/main/src/GitService.ts b/packages/main/src/GitService.ts index 0b59efc..ba274cd 100644 --- a/packages/main/src/GitService.ts +++ b/packages/main/src/GitService.ts @@ -3,6 +3,8 @@ import { BrowserWindow } from 'electron'; import { spawn } from 'child_process'; +import FS from 'fs'; +import {LocalFileSystemService} from './LocalFileSystemService.ts'; const PATH = require('path'); @@ -28,22 +30,30 @@ export const GitService = { let error = false; let output = ''; - const handleOutput = data => { - if(!data) return; - let dataAsString = data.toString(); - dataAsString = dataAsString.replace(/\n|\r/g, '\n'); - - output += dataAsString; - if(dataAsString.toLowerCase().includes('error')) - error = true; - - for(let row of dataAsString.split('\n')){ - if(row==='') continue; - window.webContents.send('GitService.MSG', row); - } - }; - p.stdout.on('data', handleOutput); - p.stderr.on('data', handleOutput); + if(o.pipe) { + console.log(o.pipe) + LocalFileSystemService.enforcePath(null,o.pipe.split('/').slice(0,-1).join('/')); + const write_stream = FS.createWriteStream(o.pipe, {flags: 'w'}); + p.stdout.pipe(write_stream); + p.stderr.pipe(write_stream); + } else { + const handleOutput = data => { + if(!data) return; + let dataAsString = data.toString(); + dataAsString = dataAsString.replace(/\n|\r/g, '\n'); + + output += dataAsString; + if(dataAsString.toLowerCase().includes('error')) + error = true; + + for(let row of dataAsString.split('\n')){ + if(row==='' || o.silent) continue; + window.webContents.send('GitService.MSG', row); + } + }; + p.stdout.on('data', handleOutput); + p.stderr.on('data', handleOutput); + } // This hits when the child process cannot be spawned p.on('error', err => { diff --git a/packages/renderer/src/ArcControlService.ts b/packages/renderer/src/ArcControlService.ts index 232e909..805145a 100644 --- a/packages/renderer/src/ArcControlService.ts +++ b/packages/renderer/src/ArcControlService.ts @@ -23,12 +23,14 @@ let init: { arc_root: undefined | string , busy: boolean, arc: null | ARC, - git_initialized: boolean + git_initialized: boolean, + skip_fs_updates: boolean, } = { arc_root: undefined , busy: false, arc: null, - git_initialized: false + git_initialized: false, + skip_fs_updates: false } function relative_to_absolute_path(relativePath: string) { @@ -221,6 +223,7 @@ const ArcControlService = { }, updateARCfromFS: async ([path,type]) => { + if(ArcControlService.props.skip_fs_updates) return; // track add/rm assays/studies through file explorer const requires_update = path.includes('isa.assay.xlsx') || path.includes('isa.study.xlsx'); if(!requires_update) return; diff --git a/packages/renderer/src/dialogs/GitDialog.vue b/packages/renderer/src/dialogs/GitDialog.vue index 2577b10..34fa5e2 100644 --- a/packages/renderer/src/dialogs/GitDialog.vue +++ b/packages/renderer/src/dialogs/GitDialog.vue @@ -5,7 +5,9 @@ import { ref, reactive, onMounted, onUnmounted } from 'vue'; export interface Props { title: String, ok_title: String, + ok_icon?: String, cancel_title: String, + cancel_icon?: String, state: Number, }; const props = defineProps(); @@ -78,8 +80,8 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC - - + + diff --git a/packages/renderer/src/views/GitCommitView.vue b/packages/renderer/src/views/GitCommitView.vue index 062959a..4715781 100644 --- a/packages/renderer/src/views/GitCommitView.vue +++ b/packages/renderer/src/views/GitCommitView.vue @@ -56,6 +56,7 @@ const getStatus = async()=>{ const sizes = await window.ipc.invoke('LocalFileSystemService.getFileSizes', status.map(x=> ArcControlService.props.arc_root +'/'+x[1])); for(let i in sizes) status[i].push(sizes[i]); + console.log(status) iProps.git_status = status; }; @@ -273,7 +274,7 @@ onUnmounted(()=>{ - Here the changes to all files in your ARC are displayed together with the file size:
+ Here the changes to all files in your ARC are displayed together with the file size:
  • : No changes
  • : The file was edited
  • @@ -283,9 +284,9 @@ onUnmounted(()=>{ Changes - + - + @@ -313,13 +314,11 @@ onUnmounted(()=>{ Click RESET to undo your latest changes and convert the ARC to the last saved commit - - - + diff --git a/packages/renderer/src/views/GitSyncView.vue b/packages/renderer/src/views/GitSyncView.vue index c3c0b2a..bfb6735 100644 --- a/packages/renderer/src/views/GitSyncView.vue +++ b/packages/renderer/src/views/GitSyncView.vue @@ -10,6 +10,9 @@ import ArcControlService from '../ArcControlService.ts'; import AppProperties from '../AppProperties.ts'; +import { Xlsx } from '@fslab/fsspreadsheet/Xlsx.js'; +import { Json } from '@fslab/fsspreadsheet/Json.js'; + import AddRemoteDialog from '../dialogs/AddRemoteDialog.vue'; import GitDialog from '../dialogs/GitDialog.vue'; import ConfirmationDialog from '../dialogs/ConfirmationDialog.vue'; @@ -145,6 +148,120 @@ const push = async()=>{ dialogProps.state=1; }; +const merge_xlsx = async (rel_path,pointers) => { + for(const pointer of pointers){ + const path = `/tmp/arc_merge/${pointer}/${rel_path}`; + + // get xlsx file + await window.ipc.invoke('GitService.run', { + args: [`show`,`${pointer}:${rel_path}`], + cwd: ArcControlService.props.arc_root, + pipe: path + }); + + // write json representation + const buffer = await window.ipc.invoke('LocalFileSystemService.readFile', [path,{}]); + const wb = await Xlsx.fromBytes(buffer); + const json = Json.toRowsJsonString(wb,1,true); + await window.ipc.invoke('LocalFileSystemService.writeFile', [path+'.json',json]); + + } + + // merge json representations + const merged_json = await window.ipc.invoke('GitService.run', { + args: [`merge-file`,`-p`, + `/tmp/arc_merge/${pointers[0]}/${rel_path}.json`, + `/tmp/arc_merge/${pointers[1]}/${rel_path}.json`, + `/tmp/arc_merge/${pointers[2]}/${rel_path}.json`, + ], + cwd: ArcControlService.props.arc_root, + silent: true + }); + + // write merged workbooks + const merged_wb = Json.fromRowsJsonString(merged_json[1]); + const merged_buffer = await Xlsx.toBytes(merged_wb); + await window.ipc.invoke( + 'LocalFileSystemService.writeFile', + [ + `${ArcControlService.props.arc_root}/${rel_path}`, + merged_buffer, + {} + ] + ); + await window.ipc.invoke('GitService.run', { + args: [`add`,rel_path], + cwd: ArcControlService.props.arc_root + }); +}; + +const merge = async ()=>{ + const dialogProps = reactive({ + title: 'Merge Divergent Branches', + ok_title: 'Ok', + cancel_title: 'Cancel', + state: 0, + }); + + $q.dialog({ + component: GitDialog, + componentProps: dialogProps + }).onOk(async ()=>{ + await ArcControlService.readARC(); + await checkRemotes(); + }).onCancel(async ()=>{ + const rebase = await window.ipc.invoke('GitService.run', { + args: [`reset`,`--hard`,`ORIG_HEAD`], + cwd: ArcControlService.props.arc_root + }); + }); + + const branches = await getBranches(); + if(!branches.current) return; + + // clean helper function + const clean = x => x[1].split('\n')[0].trim(); + + // get latest commit hash + const local_commit = clean(await window.ipc.invoke('GitService.run', { + args: [`rev-parse`,`HEAD`], + cwd: ArcControlService.props.arc_root + })); + + // get common ancestor + const common_ancestor = clean(await window.ipc.invoke('GitService.run', { + args: [`merge-base`,`HEAD`,iProps.remote+'/'+branches.current], + cwd: ArcControlService.props.arc_root + })); + + // initiate rebase + const rebase = await window.ipc.invoke('GitService.run', { + args: [`rebase`,iProps.remote+'/'+branches.current], + cwd: ArcControlService.props.arc_root + }); + + // get conflicts + const conflicts = rebase[1].split('\n').filter(r=>r.startsWith('CONFLICT')).map(r=>r.split('Merge conflict in ').pop()); + + // attempt to resolve conflicts + ArcControlService.props.skip_fs_updates = true; + for(let file of conflicts.filter(f=>f.endsWith('.xlsx'))) + await merge_xlsx(file,[local_commit,common_ancestor,iProps.remote+'/'+branches.current]); + ArcControlService.props.skip_fs_updates = false; + + await window.ipc.invoke('GitService.run', { + args: [`commit`,'--ammend'], + cwd: ArcControlService.props.arc_root + }); + + await window.ipc.invoke('GitService.run', { + args: [`-c`,`core.editor=true`,`rebase`,'--continue'], + cwd: ArcControlService.props.arc_root + }); + + dialogProps.state=1; +}; + const pull = async()=>{ await getStatus(); if(iProps.git_status.length>0) @@ -155,12 +272,18 @@ const pull = async()=>{ ok_title: 'Ok', cancel_title: null, state: 0, + needs_merge: false }); $q.dialog({ component: GitDialog, componentProps: dialogProps }).onOk(async ()=>{ + if(dialogProps.needs_merge) + await merge(); + else + await checkRemotes(); + }).onCancel(async ()=>{ await checkRemotes(); }); @@ -187,6 +310,12 @@ const pull = async()=>{ GIT_LFS_SKIP_SMUDGE: iProps.use_lfs?0:1 } }); + dialogProps.needs_merge = !response[0] && response[1].includes('You have divergent branches and need to specify how to reconcile them.'); + if(dialogProps.needs_merge){ + dialogProps.ok_title = 'Merge'; + dialogProps.cancel_title = 'Cancel'; + dialogProps.ok_icon = 'merge'; + } if(iProps.use_lfs){ response = await window.ipc.invoke('GitService.run', { args: [`lfs`,`pull`,iProps.remote,branches.current], @@ -356,7 +485,7 @@ const inspectArc = url =>{ - + Managing remotes
    • : Add additional remote connections
    • @@ -378,7 +507,7 @@ const inspectArc = url =>{
      Check this box to synchronize large files -
      + Data up- and downloads taking more than one hour require a personal access token.
      Click to add a remote with a personal access token.