Repository navigation
Expand file tree
/
Copy pathcontroller.go
More file actions
2236 lines (2136 loc) · 73.3 KB
/
Copy pathcontroller.go
File metadata and controls
2236 lines (2136 loc) · 73.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
goruntime "runtime"
"slices"
"strings"
"sync"
coreapp "github.com/tadazly/sheetproof/internal/app"
"github.com/tadazly/sheetproof/internal/diagnostics"
"github.com/tadazly/sheetproof/internal/diff"
"github.com/tadazly/sheetproof/internal/fileopener"
"github.com/tadazly/sheetproof/internal/localization"
"github.com/tadazly/sheetproof/internal/preferences"
"github.com/tadazly/sheetproof/internal/repository"
"github.com/tadazly/sheetproof/internal/ugit"
"github.com/tadazly/sheetproof/internal/workbook"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
const (
modeFiles = "files"
modeRepository = "repository"
)
type RepositoryView struct {
Name string `json:"name"`
Path string `json:"path"`
CurrentBranch string `json:"currentBranch"`
Detached bool `json:"detached"`
WorkspaceDirty bool `json:"workspaceDirty"`
Operation string `json:"operation"`
Files []string `json:"files"`
DifferenceFiles []string `json:"differenceFiles"`
DifferenceIndexing bool `json:"differenceIndexing"`
Branches []repository.Branch `json:"branches"`
DefaultRef string `json:"defaultRef"`
SelectedFile string `json:"selectedFile"`
SelectedRef string `json:"selectedRef"`
LeftState string `json:"leftState"`
RightState string `json:"rightState"`
LeftMessage string `json:"leftMessage"`
RightMessage string `json:"rightMessage"`
FileModified bool `json:"fileModified"`
SidebarWidth int `json:"sidebarWidth"`
Notice string `json:"notice"`
Loading bool `json:"loading"`
LoadGeneration uint64 `json:"loadGeneration"`
ComparisonActive bool `json:"comparisonActive"`
}
type RepositoryResult struct {
Repository RepositoryView `json:"repository"`
Summary *coreapp.Summary `json:"summary"`
}
type RecentRepository struct {
Name string `json:"name"`
Path string `json:"path"`
Available bool `json:"available"`
}
type Controller struct {
mu sync.Mutex
loadMu sync.Mutex
prefsMu sync.Mutex
wg sync.WaitGroup
ctx context.Context
session *coreapp.Session
left string
right string
options coreapp.Options
loading bool
loadErr string
mode string
prefs preferences.Store
repo *repository.Repository
repositoryView RepositoryView
tempRight string
loadGeneration uint64
differenceIndexGeneration uint64
differenceIndexCancel context.CancelFunc
shuttingDown bool
switchPrompt func() (string, error)
dialog func(context.Context, runtime.MessageDialogOptions) (string, error)
clipboardRead func(context.Context) (string, error)
clipboardWrite func(context.Context, string) error
openWorkbook func(string) error
revealWorkbook func(string) error
diagnosticLog *diagnostics.Logger
}
func NewController(left, right string, options coreapp.Options) *Controller {
return newControllerWithPreferences(left, right, options, preferences.NewStore())
}
func newControllerWithPreferences(left, right string, options coreapp.Options, prefs preferences.Store) *Controller {
if options.Locale != "" && !options.LocaleExplicit {
if preference := prefs.LanguagePreference(); preference != "system" {
options.Locale = preference
}
}
return &Controller{
left: left, right: right, options: options,
prefs: prefs, dialog: showChoiceDialog,
clipboardRead: runtime.ClipboardGetText, clipboardWrite: runtime.ClipboardSetText,
openWorkbook: fileopener.OpenDefault, revealWorkbook: fileopener.Reveal,
}
}
func (c *Controller) ask(ctx context.Context, options runtime.MessageDialogOptions) (string, error) {
if c.dialog != nil {
return c.dialog(ctx, options)
}
return showChoiceDialog(ctx, options)
}
func (c *Controller) startup(ctx context.Context) {
if c.diagnosticLog != nil {
c.diagnosticLog.Record("app.lifecycle.startup", nil)
}
c.mu.Lock()
c.ctx = ctx
repositoryPath := c.options.RepositoryPath
if repositoryPath == "" && c.left == "" && c.right == "" {
c.prefsMu.Lock()
repositoryPath = c.prefs.LastRepository()
c.prefsMu.Unlock()
}
shouldLoadFiles := c.left != "" && c.right != ""
shouldLoadRepository := !shouldLoadFiles && repositoryPath != ""
c.loading = shouldLoadFiles || shouldLoadRepository
c.mu.Unlock()
if ctx.Value("events") != nil {
runtime.OnFileDrop(ctx, c.handleFileDrop)
}
if shouldLoadFiles {
c.runStartup(func() error {
c.openInitialFiles()
return nil
})
return
}
if shouldLoadRepository {
c.runStartup(func() error {
_, err := c.openRepositoryInternal(
repositoryPath, c.options.RepositoryFile, c.options.RepositoryRef, true, false,
)
if err != nil && c.options.RepositoryPath == "" {
return fmt.Errorf("%s: %w", uiText(c.options.Locale,
"The most recently opened repository is unavailable",
"最近打开的仓库不可用",
"前回開いたリポジトリを利用できません",
), err)
}
return err
})
}
}
func (c *Controller) runStartup(task func() error) {
c.wg.Add(1)
go func() {
defer c.wg.Done()
err := task()
c.mu.Lock()
c.loading = false
if err != nil {
c.loadErr = err.Error()
}
c.mu.Unlock()
}()
}
func (c *Controller) openInitialFiles() {
c.mu.Lock()
left, right, options := c.left, c.right, c.options
c.mu.Unlock()
session, err := coreapp.Open(left, right, options)
c.mu.Lock()
defer c.mu.Unlock()
if err != nil {
c.loadErr = err.Error()
return
}
c.session = session
c.mode = modeFiles
}
type BootstrapState struct {
Loading bool `json:"loading"`
HasSession bool `json:"hasSession"`
Error string `json:"error"`
Mode string `json:"mode"`
Platform string `json:"platform"`
Repository *RepositoryView `json:"repository,omitempty"`
}
type UGitConfigurationResult struct {
Configured bool `json:"configured"`
Changed bool `json:"changed"`
Cancelled bool `json:"cancelled"`
ExecutablePath string `json:"executablePath"`
Message string `json:"message"`
}
func (c *Controller) LanguagePreference() string {
c.mu.Lock()
explicit := c.options.LocaleExplicit
locale := c.options.Locale
c.mu.Unlock()
if explicit {
return locale
}
c.prefsMu.Lock()
defer c.prefsMu.Unlock()
return c.prefs.LanguagePreference()
}
func (c *Controller) SetLanguagePreference(preference string) error {
c.prefsMu.Lock()
defer c.prefsMu.Unlock()
return c.prefs.RecordLanguagePreference(preference)
}
func (c *Controller) SetRuntimeLocale(value string) error {
locale, err := localization.Parse(value)
if err != nil {
return err
}
c.mu.Lock()
c.options.Locale = string(locale)
c.mu.Unlock()
return nil
}
// ClearDifferenceIndexCache removes only persisted semantic repository index
// results. The current comparison and saved repository history remain open.
func (c *Controller) ClearDifferenceIndexCache() error {
return c.clearStoredApplicationData(false)
}
// ClearAllData removes persisted application data without touching any
// repository, workbook, or Git state. The active in-memory comparison remains
// available until the app closes, but the next launch starts without a repo.
func (c *Controller) ClearAllData() error {
return c.clearStoredApplicationData(true)
}
func (c *Controller) clearStoredApplicationData(all bool) error {
c.mu.Lock()
c.differenceIndexGeneration++
if c.differenceIndexCancel != nil {
c.differenceIndexCancel()
c.differenceIndexCancel = nil
}
c.repositoryView.DifferenceIndexing = false
diagnosticLog := c.diagnosticLog
c.prefsMu.Lock()
var err error
if all {
err = c.prefs.ClearAllData()
} else {
err = c.prefs.ClearRepositoryIndexes()
}
c.prefsMu.Unlock()
c.mu.Unlock()
if all && err == nil && diagnosticLog != nil {
diagnosticLog.Clear()
diagnosticLog.Record("diagnostics.cleared", nil)
}
return err
}
type ExternalReloadResult struct {
Summary coreapp.Summary `json:"summary"`
Repository *RepositoryView `json:"repository,omitempty"`
Notice string `json:"notice"`
}
func (c *Controller) Bootstrap() BootstrapState {
c.mu.Lock()
defer c.mu.Unlock()
state := BootstrapState{
Loading: c.loading, HasSession: c.session != nil, Error: c.loadErr, Mode: c.mode,
Platform: goruntime.GOOS,
}
if c.repo != nil {
view := cloneRepositoryView(c.repositoryView)
state.Repository = &view
}
return state
}
func (c *Controller) ConfigureUGit() (UGitConfigurationResult, error) {
locale := c.options.Locale
executablePath, err := ugit.CurrentExecutablePath()
if err != nil {
return UGitConfigurationResult{}, err
}
c.mu.Lock()
ctx := c.ctx
c.mu.Unlock()
if ctx == nil {
ctx = context.Background()
}
inspection, err := ugit.Inspect(ctx, executablePath)
if err != nil {
return UGitConfigurationResult{}, err
}
if inspection.Configured {
return UGitConfigurationResult{
Configured: true, ExecutablePath: executablePath,
Message: uiText(locale,
"UGit's *.xlsx diff and merge tools already point to this app. No update is needed.",
"UGit 的 *.xlsx 差异与合并工具已指向当前应用,无需更新。",
"UGit の *.xlsx 差分・マージツールは、すでにこのアプリを参照しています。更新は不要です。",
) + "\n" + ugitConfigurationContext(inspection, locale),
}, nil
}
if ctx.Value("frontend") == nil {
return UGitConfigurationResult{}, errors.New(uiText(locale,
"UGit configuration cannot be confirmed without the desktop interface.",
"无界面模式下无法确认修改 UGit 配置。",
"画面を表示しないモードでは、UGit 設定の変更を確認できません。",
))
}
message := fmt.Sprintf(
uiText(locale,
"Set this app as UGit's diff and merge tool for *.xlsx files:\n%s\n\n%s\n\nOnly *.xlsx settings will change. Other file types are not affected.",
"将把当前应用设为 UGit 的 *.xlsx 差异与合并工具:\n%s\n\n%s\n\n只会替换 *.xlsx 相关配置,不影响其他文件类型。",
"このアプリを UGit の *.xlsx 差分・マージツールに設定します:\n%s\n\n%s\n\n変更するのは *.xlsx の設定だけです。ほかのファイル形式には影響しません。",
),
executablePath, ugitConfigurationContext(inspection, locale),
)
if inspection.NeedsUpdate {
if len(inspection.ExistingPaths) > 0 {
message += uiText(locale, "\n\nOld paths to replace:\n• ", "\n\n需要替换的旧路径:\n• ", "\n\n置き換える古いパス:\n• ") + strings.Join(inspection.ExistingPaths, "\n• ")
} else {
message += uiText(locale,
"\n\nThe existing *.xlsx tool settings are incomplete or incompatible and will be repaired.",
"\n\n现有 *.xlsx 工具配置不完整或不兼容,将一并修复。",
"\n\n既存の *.xlsx ツール設定が不完全または互換性のない状態です。あわせて修復します。",
)
}
}
cancel := uiText(locale, "Cancel", "取消", "キャンセル")
configure := uiText(locale, "Configure UGit", "配置 UGit", "UGit を設定")
answer, err := c.ask(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog, Title: configure,
Message: message, Buttons: []string{cancel, configure},
DefaultButton: configure, CancelButton: cancel,
})
if err != nil {
return UGitConfigurationResult{}, err
}
if answer != configure {
return UGitConfigurationResult{
Cancelled: true, ExecutablePath: executablePath,
Message: uiText(locale, "UGit settings were not changed.", "未修改 UGit 配置。", "UGit の設定は変更されていません。"),
}, nil
}
updated, err := ugit.Configure(ctx, executablePath)
if err != nil {
return UGitConfigurationResult{}, fmt.Errorf("%s: %w", uiText(locale, "configure UGit", "配置 UGit", "UGit の設定"), err)
}
return UGitConfigurationResult{
Configured: updated.Configured, Changed: true, ExecutablePath: executablePath,
Message: uiText(locale,
"UGit's *.xlsx diff and merge tools have been updated. If UGit is running, restart it before testing.",
"UGit 的 *.xlsx 差异与合并工具已更新。如果 UGit 正在运行,请重启后再测试。",
"UGit の *.xlsx 差分・マージツールを更新しました。UGit を起動中の場合は、再起動してから確認してください。",
) + "\n" + ugitConfigurationContext(updated, locale),
}, nil
}
func ugitConfigurationContext(inspection ugit.Inspection, locale string) string {
origins := uiText(locale, "No existing *.xlsx settings", "尚无 *.xlsx 配置", "既存の *.xlsx 設定なし")
if len(inspection.ConfigOrigins) > 0 {
origins = strings.Join(inspection.ConfigOrigins, uiText(locale, ", ", "、", "、"))
}
return fmt.Sprintf(uiText(locale, "Git: %s\n*.xlsx setting source: %s", "Git:%s\n*.xlsx 配置来源:%s", "Git:%s\n*.xlsx 設定の参照元:%s"), inspection.GitPath, origins)
}
func (c *Controller) shutdown(_ context.Context) {
if c.diagnosticLog != nil {
c.diagnosticLog.Record("app.lifecycle.shutdown.start", nil)
}
c.mu.Lock()
c.shuttingDown = true
c.differenceIndexGeneration++
cancelDifferenceIndex := c.differenceIndexCancel
c.differenceIndexCancel = nil
c.mu.Unlock()
if cancelDifferenceIndex != nil {
cancelDifferenceIndex()
}
c.wg.Wait()
c.mu.Lock()
session, temp := c.session, c.tempRight
c.session, c.tempRight = nil, ""
c.mu.Unlock()
if session != nil {
_ = session.Close()
}
removeTemporary(temp)
if c.diagnosticLog != nil {
c.diagnosticLog.Record("app.lifecycle.shutdown.finish", nil)
}
}
func (c *Controller) beforeClose(ctx context.Context) bool {
c.mu.Lock()
session := c.session
c.mu.Unlock()
dirty := session != nil && session.Dirty()
if c.diagnosticLog != nil {
c.diagnosticLog.Record("app.lifecycle.before-close", map[string]any{"dirty": dirty})
}
if !dirty {
return false
}
locale := c.options.Locale
save := uiText(locale, "Save and continue", "保存并继续", "保存して続行")
discard := uiText(locale, "Continue without saving", "不保存并继续", "保存せずに続行")
cancel := uiText(locale, "Cancel", "取消", "キャンセル")
answer, err := c.ask(ctx, runtime.MessageDialogOptions{
Type: runtime.QuestionDialog, Title: uiText(locale, "Unsaved changes", "有未保存的修改", "未保存の変更"),
Message: uiText(locale, "This workbook has unsaved changes.", "当前工作簿有未保存的修改。", "このブックには未保存の変更があります。"),
Buttons: []string{save, discard, cancel},
DefaultButton: save, CancelButton: cancel,
})
if err != nil {
return true
}
switch answer {
case save:
_, err := c.Save()
return err != nil
case discard:
return false
default:
return true
}
}
func (c *Controller) SelectAndOpen() (coreapp.Summary, error) {
c.mu.Lock()
ctx := c.ctx
c.mu.Unlock()
locale := c.options.Locale
filter := []runtime.FileFilter{{DisplayName: uiText(locale, "Excel workbook (*.xlsx)", "Excel 工作簿 (*.xlsx)", "Excel ブック (*.xlsx)"), Pattern: "*.xlsx"}}
left, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{Title: uiText(locale, "Select the left (local) workbook", "选择左侧(本地)工作簿", "左側(ローカル)のブックを選択"), Filters: filter})
if err != nil {
return coreapp.Summary{}, err
}
if left == "" {
return coreapp.Summary{}, fmt.Errorf("%s", uiText(locale, "Left file selection was cancelled.", "已取消选择左侧文件。", "左側ファイルの選択をキャンセルしました。"))
}
right, err := runtime.OpenFileDialog(ctx, runtime.OpenDialogOptions{Title: uiText(locale, "Select the right (comparison) workbook", "选择右侧(对比来源)工作簿", "右側(比較元)のブックを選択"), Filters: filter})
if err != nil {
return coreapp.Summary{}, err
}
if right == "" {
return coreapp.Summary{}, fmt.Errorf("%s", uiText(locale, "Right file selection was cancelled.", "已取消选择右侧文件。", "右側ファイルの選択をキャンセルしました。"))
}
return c.OpenFiles(left, right)
}
func (c *Controller) OpenFiles(left, right string) (coreapp.Summary, error) {
session, err := coreapp.Open(left, right, c.options)
if err != nil {
return coreapp.Summary{}, err
}
if err := c.confirmSessionSwitch(); err != nil {
_ = session.Close()
return coreapp.Summary{}, err
}
c.mu.Lock()
old, oldTemp := c.session, c.tempRight
c.session = session
c.left, c.right = left, right
c.tempRight = ""
c.repo = nil
c.repositoryView = RepositoryView{}
c.mode = modeFiles
c.loadErr = ""
c.loading = false
c.loadGeneration++
c.mu.Unlock()
if old != nil {
_ = old.Close()
}
removeTemporary(oldTemp)
return session.Summary(), nil
}
func (c *Controller) SelectRepository() (RepositoryResult, error) {
c.mu.Lock()
ctx := c.ctx
locale := c.options.Locale
c.mu.Unlock()
path, err := runtime.OpenDirectoryDialog(ctx, runtime.OpenDialogOptions{
Title: uiText(locale, "Open local Git repository", "打开本地 Git 仓库", "ローカルの Git リポジトリを開く"),
})
if err != nil {
return RepositoryResult{}, err
}
if path == "" {
return RepositoryResult{}, errors.New(uiText(locale, "Repository selection was cancelled.", "已取消选择仓库。", "リポジトリの選択をキャンセルしました。"))
}
return c.OpenRepository(path)
}
func (c *Controller) RecentRepositories() []RecentRepository {
c.prefsMu.Lock()
paths := c.prefs.RecentRepositories()
c.prefsMu.Unlock()
result := make([]RecentRepository, 0, len(paths))
for _, path := range paths {
info, err := os.Stat(path)
result = append(result, RecentRepository{
Name: filepath.Base(path),
Path: path,
Available: err == nil && info.IsDir(),
})
}
return result
}
func (c *Controller) OpenRepository(path string) (RepositoryResult, error) {
return c.openRepositoryInternal(path, "", "", true, true)
}
func (c *Controller) openRepositoryInternal(
path, selectedFile, selectedRef string, remember, confirm bool,
) (RepositoryResult, error) {
locale := c.options.Locale
repo, info, err := repository.Open(path)
if err != nil {
return RepositoryResult{}, err
}
if selectedFile != "" && !slices.Contains(info.Files, filepath.ToSlash(selectedFile)) {
return RepositoryResult{}, fmt.Errorf(uiText(locale,
"The repository does not contain the specified XLSX file: %s",
"仓库中不存在指定的 XLSX 文件:%s",
"指定された XLSX ファイルはリポジトリにありません:%s"), selectedFile)
}
if selectedRef != "" {
if _, err := repo.ResolveReference(selectedRef, info.Branches); err != nil {
return RepositoryResult{}, err
}
}
if confirm {
if err := c.confirmSessionSwitch(); err != nil {
return RepositoryResult{}, err
}
}
c.loadMu.Lock()
defer c.loadMu.Unlock()
c.prefsMu.Lock()
repositoryWidth := c.prefs.RepositoryWidth()
preferredRef := c.prefs.RepositoryRef(info.Root)
c.prefsMu.Unlock()
view := viewFromInfo(info, repositoryWidth, locale)
if selectedRef != "" {
branch, _ := repo.ResolveReference(selectedRef, info.Branches)
view.SelectedRef = branch.FullName
} else if preferredRef != "" {
if branch, resolveErr := repo.ResolveReference(preferredRef, info.Branches); resolveErr == nil {
view.SelectedRef = branch.FullName
}
}
if view.SelectedRef == "" {
view.SelectedRef = view.DefaultRef
}
var differenceBranch repository.Branch
var differenceSignature string
if view.SelectedRef != "" {
differenceBranch, _ = repo.ResolveReference(view.SelectedRef, info.Branches)
differenceFiles, signature, cached, diffErr := c.prepareRepositoryDifferenceIndex(
repo, differenceBranch, view.Files, true,
)
if diffErr != nil {
view.Notice = fmt.Sprintf(uiText(locale,
"The repository opened, but the changed-workbook list could not be loaded: %v",
"仓库已打开,但差异表加载失败:%v",
"リポジトリは開きましたが、差分のあるブック一覧を読み込めませんでした:%v"), diffErr)
} else {
view.DifferenceFiles = differenceFiles
view.DifferenceIndexing = !cached
differenceSignature = signature
}
}
c.mu.Lock()
old, oldTemp := c.session, c.tempRight
c.session, c.tempRight = nil, ""
c.repo = repo
c.repositoryView = view
c.mode = modeRepository
c.loadErr = ""
c.loadGeneration++
c.repositoryView.LoadGeneration = c.loadGeneration
c.mu.Unlock()
if old != nil {
_ = old.Close()
}
removeTemporary(oldTemp)
if remember {
c.prefsMu.Lock()
preferenceErr := c.prefs.RecordRepository(info.Root)
c.prefsMu.Unlock()
if preferenceErr != nil {
c.mu.Lock()
c.repositoryView.Notice = appendNotice(c.repositoryView.Notice, uiText(locale,
"The repository opened, but it could not be added to recent repositories.",
"仓库已打开,但无法记录为最近仓库。",
"リポジトリは開きましたが、最近使ったリポジトリに記録できませんでした。"))
c.mu.Unlock()
}
c.recordRepositoryRef(info.Root, view.SelectedRef)
}
if view.DifferenceIndexing {
c.startRepositoryDifferenceIndex(repo, differenceBranch, view.Files, differenceSignature)
}
if selectedFile != "" {
return c.selectRepositoryFileLocked(filepath.ToSlash(selectedFile), selectedRef)
}
return c.Repository()
}
func viewFromInfo(info repository.Info, width int, locale string) RepositoryView {
rightState := "no-ref"
if len(info.Branches) == 0 {
rightState = "no-branches"
}
return RepositoryView{
Name: info.Name, Path: info.Root, CurrentBranch: info.CurrentBranch,
Detached: info.Detached, WorkspaceDirty: info.Dirty, Operation: info.Operation,
Files: append([]string{}, info.Files...), DifferenceFiles: []string{},
Branches: append([]repository.Branch{}, info.Branches...),
DefaultRef: info.DefaultRef, LeftState: "no-file", RightState: rightState,
LeftMessage: uiText(locale,
"Select an XLSX file from the repository tree.",
"请先在左侧目录树中选择一个 XLSX 表格。",
"リポジトリツリーから XLSX ファイルを選択してください。"),
RightMessage: uiText(locale,
"Select an XLSX file from the repository tree.",
"请先在左侧目录树中选择一个 XLSX 表格。",
"リポジトリツリーから XLSX ファイルを選択してください。"),
SidebarWidth: width,
}
}
func (c *Controller) Repository() (RepositoryResult, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.repo == nil {
return RepositoryResult{}, errors.New(uiText(c.options.Locale, "No Git repository is open.", "尚未打开 Git 仓库。", "Git リポジトリが開かれていません。"))
}
result := RepositoryResult{Repository: cloneRepositoryView(c.repositoryView)}
if c.session != nil {
summary := c.session.Summary()
result.Summary = &summary
}
return result, nil
}
func (c *Controller) SelectRepositoryFile(relative string) (RepositoryResult, error) {
if err := c.confirmRepositoryFileSwitch(relative); err != nil {
return RepositoryResult{}, err
}
c.loadMu.Lock()
defer c.loadMu.Unlock()
return c.selectRepositoryFileLocked(filepath.ToSlash(relative), "")
}
func (c *Controller) selectRepositoryFileLocked(relative, requestedRef string) (RepositoryResult, error) {
c.mu.Lock()
repo := c.repo
locale := c.options.Locale
view := cloneRepositoryView(c.repositoryView)
c.loadGeneration++
generation := c.loadGeneration
c.repositoryView.Loading = true
c.repositoryView.LoadGeneration = generation
c.mu.Unlock()
if repo == nil {
return RepositoryResult{}, errors.New(uiText(locale, "No Git repository is open.", "尚未打开 Git 仓库。", "Git リポジトリが開かれていません。"))
}
if !slices.Contains(view.Files, relative) {
c.finishRepositoryLoadError(generation)
return RepositoryResult{}, fmt.Errorf(uiText(locale,
"The repository does not contain this XLSX file: %s",
"仓库中不存在该 XLSX 文件:%s",
"この XLSX ファイルはリポジトリにありません:%s"), relative)
}
leftPath, err := repo.ResolveRelativePath(relative)
if err != nil {
c.finishRepositoryLoadError(generation)
return RepositoryResult{}, err
}
if stat, err := os.Stat(leftPath); err != nil || stat.IsDir() {
return c.installRepositoryFileFailure(
generation, repo, relative, "missing",
fmt.Sprintf(uiText(locale,
"This workbook is missing from the worktree.\nPath: %s",
"当前工作区中不存在该表格。\n路径:%s",
"このブックはワークツリーにありません。\nパス:%s"), relative),
)
}
refValue := requestedRef
if refValue == "" && view.SelectedRef != "" {
refValue = view.SelectedRef
}
if refValue == "" {
refValue = view.DefaultRef
}
options := c.options
options.Output = ""
options.ReadonlyLeft = false
options.LeftLabel = workspaceLabel(view, c.options.Locale)
options.RightLabel = comparisonRevisionLabel(c.options.Locale)
session, err := coreapp.OpenLeft(leftPath, options)
if err != nil {
return c.installRepositoryFileFailure(
generation, repo, relative, "error",
fmt.Sprintf(uiText(locale,
"This workbook could not be opened.\n%v",
"无法打开该表格。\n%v",
"このブックを開けませんでした。\n%v"), err),
)
}
rightState, rightMessage, tempPath, selectedFullRef := c.loadRight(repo, view, session, refValue, relative)
modified, statusErr := repo.FileModified(relative)
if statusErr != nil {
_ = session.Close()
removeTemporary(tempPath)
c.finishRepositoryLoadError(generation)
return RepositoryResult{}, statusErr
}
c.mu.Lock()
if generation != c.loadGeneration || repo != c.repo {
c.mu.Unlock()
_ = session.Close()
removeTemporary(tempPath)
return RepositoryResult{}, errors.New(uiText(locale,
"A newer selection replaced this load request.",
"加载结果已被新的选择替代。",
"別の項目が選択されたため、この読み込み結果は破棄されました。"))
}
old, oldTemp := c.session, c.tempRight
c.session, c.tempRight = session, tempPath
c.repositoryView.SelectedFile = relative
c.repositoryView.SelectedRef = selectedFullRef
c.repositoryView.LeftState = "ready"
c.repositoryView.LeftMessage = ""
c.repositoryView.RightState = rightState
c.repositoryView.RightMessage = rightMessage
c.repositoryView.FileModified = modified
c.repositoryView.Loading = false
c.repositoryView.ComparisonActive = rightState == "ready"
c.mu.Unlock()
if old != nil {
_ = old.Close()
}
removeTemporary(oldTemp)
return c.Repository()
}
func (c *Controller) installRepositoryFileFailure(
generation uint64,
repo *repository.Repository,
relative, state, message string,
) (RepositoryResult, error) {
c.mu.Lock()
if generation != c.loadGeneration || repo != c.repo {
c.mu.Unlock()
return RepositoryResult{}, errors.New(uiText(c.options.Locale,
"A newer selection replaced this load request.",
"加载结果已被新的选择替代。",
"別の項目が選択されたため、この読み込み結果は破棄されました。"))
}
old, oldTemp := c.session, c.tempRight
c.session, c.tempRight = nil, ""
c.repositoryView.SelectedFile = relative
c.repositoryView.LeftState = state
c.repositoryView.LeftMessage = message
c.repositoryView.RightState = state
c.repositoryView.RightMessage = message
c.repositoryView.Loading = false
c.repositoryView.ComparisonActive = false
c.mu.Unlock()
if old != nil {
_ = old.Close()
}
removeTemporary(oldTemp)
return c.Repository()
}
func (c *Controller) loadRight(
repo *repository.Repository,
view RepositoryView,
session *coreapp.Session,
refValue, relative string,
) (state, message, tempPath, selectedFullRef string) {
if len(view.Branches) == 0 {
return "no-branches", uiText(c.options.Locale,
"This repository has no other revision to compare.",
"当前仓库中没有可用于对比的其他版本。",
"このリポジトリには比較できるほかのリビジョンがありません。"), "", ""
}
if refValue == "" {
return "no-ref", uiText(c.options.Locale,
"Select a Git revision to compare.",
"请选择一个用于对比的 Git 版本。",
"比較する Git リビジョンを選択してください。"), "", ""
}
branch, err := repo.ResolveReference(refValue, view.Branches)
if err != nil {
return "error", err.Error(), "", ""
}
selectedFullRef = branch.FullName
tempPath, err = repo.ReadReferenceFile(branch, relative)
if repository.IsMissingFile(err) {
return "missing",
fmt.Sprintf(uiText(c.options.Locale,
"This workbook is not present in the selected revision.\nRevision: %s\nPath: %s",
"所选版本中不存在此表格。\n版本:%s\n路径:%s",
"選択したリビジョンにこのブックはありません。\nリビジョン:%s\nパス:%s"), branch.Name, relative),
"", selectedFullRef
}
if err != nil {
return "error", err.Error(), "", selectedFullRef
}
if err := session.ReplaceRight(tempPath, readOnlyRevisionLabel(branch.Name, c.options.Locale)); err != nil {
removeTemporary(tempPath)
return "invalid",
fmt.Sprintf(uiText(c.options.Locale,
"The file in the selected revision is not a readable XLSX workbook.\n%v",
"所选版本中的文件无法作为 XLSX 工作簿打开。\n%v",
"選択したリビジョンのファイルを XLSX ブックとして開けません。\n%v"), err),
"", selectedFullRef
}
return "ready", "", tempPath, selectedFullRef
}
func (c *Controller) SelectRepositoryRef(refValue string) (RepositoryResult, error) {
c.loadMu.Lock()
defer c.loadMu.Unlock()
c.mu.Lock()
repo, session := c.repo, c.session
view := cloneRepositoryView(c.repositoryView)
if repo == nil {
c.mu.Unlock()
return RepositoryResult{}, errors.New(uiText(c.options.Locale, "No Git repository is open.", "尚未打开 Git 仓库。", "Git リポジトリが開かれていません。"))
}
if session == nil || view.SelectedFile == "" {
branch, err := repo.ResolveReference(refValue, view.Branches)
if err != nil {
c.mu.Unlock()
return RepositoryResult{}, err
}
c.mu.Unlock()
differenceFiles, signature, cached, diffErr := c.prepareRepositoryDifferenceIndex(
repo, branch, view.Files, true,
)
c.mu.Lock()
if repo != c.repo {
c.mu.Unlock()
return RepositoryResult{}, errors.New(uiText(c.options.Locale,
"A newer selection replaced this load request.",
"加载结果已被新的选择替代。",
"別の項目が選択されたため、この読み込み結果は破棄されました。"))
}
c.repositoryView.SelectedRef = branch.FullName
if diffErr != nil {
c.repositoryView.Notice = appendNotice(
c.repositoryView.Notice,
fmt.Sprintf(uiText(c.options.Locale,
"The comparison revision changed, but the changed-workbook list could not be loaded: %v",
"已切换对比版本,但差异表加载失败:%v",
"比較するリビジョンを変更しましたが、差分のあるブック一覧を読み込めませんでした:%v"), diffErr),
)
} else {
c.repositoryView.DifferenceFiles = differenceFiles
c.repositoryView.DifferenceIndexing = !cached
}
root := c.repositoryView.Path
c.mu.Unlock()
c.recordRepositoryRef(root, branch.FullName)
if diffErr == nil && !cached {
c.startRepositoryDifferenceIndex(repo, branch, view.Files, signature)
}
return c.Repository()
}
c.loadGeneration++
generation := c.loadGeneration
c.repositoryView.Loading = true
c.repositoryView.LoadGeneration = generation
c.mu.Unlock()
branch, err := repo.ResolveReference(refValue, view.Branches)
if err != nil {
c.finishRepositoryLoadError(generation)
return RepositoryResult{}, err
}
temp, readErr := repo.ReadReferenceFile(branch, view.SelectedFile)
state, message := "ready", ""
if repository.IsMissingFile(readErr) {
state = "missing"
message = fmt.Sprintf(uiText(c.options.Locale,
"This workbook is not present in the selected revision.\nRevision: %s\nPath: %s",
"所选版本中不存在此表格。\n版本:%s\n路径:%s",
"選択したリビジョンにこのブックはありません。\nリビジョン:%s\nパス:%s"), branch.Name, view.SelectedFile)
if err := session.DetachRight(readOnlyRevisionLabel(branch.Name, c.options.Locale)); err != nil {
c.finishRepositoryLoadError(generation)
return RepositoryResult{}, err
}
} else if readErr != nil {
state, message = "error", readErr.Error()
if err := session.DetachRight(readOnlyRevisionLabel(branch.Name, c.options.Locale)); err != nil {
c.finishRepositoryLoadError(generation)
return RepositoryResult{}, err
}
} else if err := session.ReplaceRight(temp, readOnlyRevisionLabel(branch.Name, c.options.Locale)); err != nil {
removeTemporary(temp)
temp = ""
state = "invalid"
message = fmt.Sprintf(uiText(c.options.Locale,
"The file in the selected revision is not a readable XLSX workbook.\n%v",
"所选版本中的文件无法作为 XLSX 工作簿打开。\n%v",
"選択したリビジョンのファイルを XLSX ブックとして開けません。\n%v"), err)
_ = session.DetachRight(readOnlyRevisionLabel(branch.Name, c.options.Locale))
}
differenceFiles, signature, cached, diffErr := c.prepareRepositoryDifferenceIndex(
repo, branch, view.Files, true,
)
c.mu.Lock()
if generation != c.loadGeneration || session != c.session {
c.mu.Unlock()
removeTemporary(temp)
return RepositoryResult{}, errors.New(uiText(c.options.Locale,
"A newer selection replaced this load request.",
"加载结果已被新的选择替代。",
"別の項目が選択されたため、この読み込み結果は破棄されました。"))
}
oldTemp := c.tempRight
c.tempRight = temp
c.repositoryView.SelectedRef = branch.FullName
c.repositoryView.RightState = state
c.repositoryView.RightMessage = message
c.repositoryView.Loading = false
c.repositoryView.ComparisonActive = state == "ready"
if diffErr != nil {
c.repositoryView.Notice = appendNotice(
c.repositoryView.Notice,
fmt.Sprintf(uiText(c.options.Locale,
"The comparison revision changed, but the changed-workbook list could not be loaded: %v",
"已切换对比版本,但差异表加载失败:%v",
"比較するリビジョンを変更しましたが、差分のあるブック一覧を読み込めませんでした:%v"), diffErr),
)
} else {
c.repositoryView.DifferenceFiles = differenceFiles
c.repositoryView.DifferenceIndexing = !cached
}
root := c.repositoryView.Path
c.mu.Unlock()
removeTemporary(oldTemp)
c.recordRepositoryRef(root, branch.FullName)
if diffErr == nil && !cached {
c.startRepositoryDifferenceIndex(repo, branch, view.Files, signature)
}
return c.Repository()
}
func (c *Controller) RefreshRepository() (RepositoryResult, error) {
c.loadMu.Lock()
defer c.loadMu.Unlock()
c.mu.Lock()
repo := c.repo
oldView := cloneRepositoryView(c.repositoryView)
c.mu.Unlock()
if repo == nil {
return RepositoryResult{}, errors.New(uiText(c.options.Locale, "No Git repository is open.", "尚未打开 Git 仓库。", "Git リポジトリが開かれていません。"))
}
info, err := repo.Refresh()
if err != nil {
return RepositoryResult{}, err
}
view := viewFromInfo(info, oldView.SidebarWidth, c.options.Locale)
view.SelectedFile = oldView.SelectedFile
view.SelectedRef = oldView.SelectedRef