Skip to content

Commit 488b043

Browse files
committed
Prevent errors when config values are in the wrong format
1 parent 531a3f8 commit 488b043

14 files changed

Lines changed: 144 additions & 43 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"remotename",
4444
"scroller",
4545
"shortstat",
46-
"vueuse"
46+
"vueuse",
47+
"yaireo"
4748
],
4849
"files.trimTrailingWhitespace": true,
4950
"eslint.experimental.useFlatConfig": true,

src/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ interface String {
44
hashCode(): number
55
}
66

7+
type Json = null | string | number | boolean | Json[] | { [key: string]: Json };
8+
79
interface BridgeMessage {
810
type: 'response-to-web' | 'request-from-web' | 'push-to-web',
911
command?: string,

src/state.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ let storage_providers = {
4343
},
4444
}
4545

46+
/** @typedef {Record<string, Json>} ActualConfig Necessary as config schema isn't enforced by VSCode */
47+
4648
/**
4749
* There can also be dynamic states, aka GitInput states.
4850
* This object only bootstraps the static ones. `state()` eventually accepts both.
@@ -52,7 +54,7 @@ let static_states = {
5254
config: {
5355
type: 'special',
5456
storage: (ctx) => ({
55-
get: () => ctx.get_config(),
57+
get: () => /** @type {ActualConfig} */ (ctx.get_config()),
5658
set() {},
5759
}),
5860
},

web/src/components/CommitRefTips.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</template>
66
<script setup>
77
import { computed } from 'vue'
8-
import { config } from '../data/store'
8+
import config from '../data/store/config'
99
1010
let props = defineProps({
1111
commit: {
@@ -27,7 +27,7 @@ function group_same_name_branches_into_one(/** @type {Branch[]} */ branches) {
2727
}
2828
2929
let grouped_git_refs = computed(() => {
30-
if (config.value['group-branch-remotes'] === false)
30+
if (config.get_boolean_or_undefined('group-branch-remotes') === false)
3131
return props.commit.refs
3232
return Object.values(props.commit.refs.reduce((/** @type {Record<string, GitRef[]>} */ all, ref) => {
3333
all[ref.name] = [...all[ref.name] || [], ref]

web/src/data/state.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ add_push_listener('state-update', (/** @template {StateKey} K @type {{data?: {ke
2929
* @param {K} key
3030
* @param {NonNullable<StateType<K>>} default_value
3131
* @param {() => any} on_load=
32+
* @param {{ write_only?: boolean }} opt
3233
*/
33-
let state = (key, default_value, on_load = () => {}) => {
34+
let state = (key, default_value, on_load = () => {}, { write_only } = {}) => {
3435
/** @type {State<K>|undefined} */
3536
let ret = _states[key]
3637
if (ret) {
@@ -61,8 +62,10 @@ let state = (key, default_value, on_load = () => {}) => {
6162
// @ts-ignore // TODO:
6263
_states[key] = ret;
6364
(async () => {
64-
await ret.reload()
65-
await nextTick()
65+
if (! write_only) {
66+
await ret.reload()
67+
await nextTick()
68+
}
6669
on_load?.()
6770
})()
6871
return ret

web/src/data/store/actions.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { computed } from 'vue'
22
import default_git_actions from '../default-git-actions.json'
3-
import { combine_branches_from_branch_name, combine_branches_to_branch_name, config } from '../store'
3+
import { combine_branches_from_branch_name, combine_branches_to_branch_name } from '../store'
44
import { git } from '../../bridge'
55
import { default_origin } from '../store/repo'
6+
import config from '../store/config'
67

78
/**
89
* @param actions {ConfigGitAction[]}
@@ -12,7 +13,7 @@ import { default_origin } from '../store/repo'
1213
function apply_action_replacements(actions, replacements = []) {
1314
let namespace = replacements.map(([k]) => k).join('-') || 'global'
1415
let replacements_by_type = replacements.reduce((all, replacement) =>
15-
/** tsc doesn't understand of this */ ((/** @type {any} */ (all)[typeof replacement[1]] ??= []).push(replacement), all), /** @type {{string:[string,string][], function:[string,()=>Promise<string>][]}} */ ({ string: [], function: [] })) // eslint-disable-line jsdoc/valid-types
16+
/** tsc doesn't understand of this */ ((/** @type {any} */ (all)[typeof replacement[1]] ??= []).push(replacement), all), /** @type {{string:[string,string][], function:[string,()=>Promise<string>][]}} */ ({ string: [], function: [] }))
1617
let apply_string_replacements = (/** @type {string} */ txt) =>
1718
replacements_by_type.string.reduce((str, replacement) =>
1819
str.replaceAll(replacement[0], replacement[1]), txt)
@@ -39,23 +40,27 @@ function apply_action_replacements(actions, replacements = []) {
3940

4041
/** @type {Vue.Ref<GitAction[]>} */
4142
export let global_actions = computed(() =>
42-
apply_action_replacements(default_git_actions['actions.global'].concat(config.value.actions?.global || [])))
43+
apply_action_replacements(/** @type {ConfigGitAction[]} */ (default_git_actions['actions.global'])
44+
.concat(config.get_git_actions('actions.global'))))
4345
export let commit_actions = (/** @type {string} */ hash) => computed(() => {
44-
let config_commit_actions = default_git_actions['actions.commit'].concat(config.value.actions?.commit || [])
46+
let config_commit_actions = /** @type {ConfigGitAction[]} */ (default_git_actions['actions.commit'])
47+
.concat(config.get_git_actions('actions.commit'))
4548
return apply_action_replacements(config_commit_actions, [
4649
['{COMMIT_HASH}', hash],
4750
['{COMMIT_BODY}', () =>
4851
git(`show -s --format="%B" ${hash}`)],
4952
['{DEFAULT_REMOTE_NAME}', default_origin.value || 'MISSING_REMOTE_NAME']])
5053
})
5154
export let commits_actions = (/** @type {string[]} */ hashes) => computed(() => {
52-
let config_commits_actions = default_git_actions['actions.commits'].concat(config.value.actions?.commits || [])
55+
let config_commits_actions = /** @type {ConfigGitAction[]} */ (default_git_actions['actions.commits'])
56+
.concat(config.get_git_actions('actions.commits'))
5357
return apply_action_replacements(config_commits_actions, [
5458
['{COMMIT_HASHES}', hashes.join(' ')],
5559
['{DEFAULT_REMOTE_NAME}', default_origin.value || 'MISSING_REMOTE_NAME']])
5660
})
5761
export let branch_actions = (/** @type {Branch} */ branch) => computed(() => {
58-
let config_branch_actions = default_git_actions['actions.branch'].concat(config.value.actions?.branch || [])
62+
let config_branch_actions = /** @type {ConfigGitAction[]} */ (default_git_actions['actions.branch'])
63+
.concat(config.get_git_actions('actions.branch'))
5964
return apply_action_replacements(config_branch_actions, [
6065
['{BRANCH_ID}', branch.id],
6166
['{BRANCH_DISPLAY_NAME}', branch.display_name],
@@ -65,19 +70,22 @@ export let branch_actions = (/** @type {Branch} */ branch) => computed(() => {
6570
['{DEFAULT_REMOTE_NAME}', default_origin.value || 'MISSING_REMOTE_NAME']])
6671
})
6772
export let tag_actions = (/** @type {string} */ tag_name) => computed(() => {
68-
let config_tag_actions = default_git_actions['actions.tag'].concat(config.value.actions?.tag || [])
73+
let config_tag_actions = /** @type {ConfigGitAction[]} */ (default_git_actions['actions.tag'])
74+
.concat(config.get_git_actions('actions.tag'))
6975
return apply_action_replacements(config_tag_actions, [
7076
['{TAG_NAME}', tag_name],
7177
['{DEFAULT_REMOTE_NAME}', default_origin.value || 'MISSING_REMOTE_NAME']])
7278
})
7379
export let stash_actions = (/** @type {string} */ stash_name) => computed(() => {
74-
let config_stash_actions = default_git_actions['actions.stash'].concat(config.value.actions?.stash || [])
80+
let config_stash_actions = /** @type {ConfigGitAction[]} */ (default_git_actions['actions.stash'])
81+
.concat(config.get_git_actions('actions.stash'))
7582
return apply_action_replacements(config_stash_actions, [
7683
['{STASH_NAME}', stash_name],
7784
['{DEFAULT_REMOTE_NAME}', default_origin.value || 'MISSING_REMOTE_NAME']])
7885
})
7986
export let combine_branches_actions = computed(() => {
80-
let config_combine_branches_actions = default_git_actions['actions.branch-drop'].concat(config.value.actions?.['branch-drop'] || [])
87+
let config_combine_branches_actions = /** @type {ConfigGitAction[]} */ (default_git_actions['actions.branch-drop'])
88+
.concat(config.get_git_actions('actions.branch-drop'))
8189
return apply_action_replacements(config_combine_branches_actions, [
8290
['{SOURCE_BRANCH_NAME}', combine_branches_from_branch_name.value],
8391
['{TARGET_BRANCH_NAME}', combine_branches_to_branch_name.value],

web/src/data/store/config.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import state from '../state'
2+
3+
// TODO: test with all set to null
4+
let config = state('config', {}).ref
5+
6+
/** @returns {val is Record<string, unknown>} */
7+
let is_object = (/** @type {Json | undefined} */ val) =>
8+
val !== null && typeof val === 'object' && ! Array.isArray(val)
9+
10+
let get_json = (/** @type {string} */ key) =>
11+
key.split('.')
12+
.reduce((/** @type {Json | undefined} */ acc, sub_key) =>
13+
is_object(acc) ? acc[sub_key] : null
14+
, config.value) ?? null
15+
16+
/** All getters support nested keys using `.` dot separation notation */
17+
export default {
18+
get_string_array: (/** @type {string} */ key) => {
19+
let val = get_json(key)
20+
return Array.isArray(val) ? val.map(String) : []
21+
},
22+
get_string_map: (/** @type {string} */ key) => {
23+
let val = get_json(key)
24+
return is_object(val)
25+
? Object.fromEntries(Object.entries(val).map(([k, v]) =>
26+
[k, String(v)]))
27+
: {}
28+
},
29+
get_number: (/** @type {string} */ key) =>
30+
Number(get_json(key)) || 0,
31+
get_string: (/** @type {string} */ key) =>
32+
String(get_json(key)),
33+
/** Undefined necessary in case `true` is supposed to be the default */
34+
get_boolean_or_undefined: (/** @type {string} */ key) => {
35+
let val = get_json(key)
36+
return val === undefined ? undefined : Boolean(val)
37+
},
38+
/** @returns {ConfigGitAction[]} */
39+
get_git_actions: (/** @type {string} */ key) => {
40+
let val = get_json(key)
41+
if (! Array.isArray(val))
42+
return []
43+
return val.map(v => {
44+
if (! is_object(v))
45+
return null
46+
return {
47+
title: String(v.title),
48+
description: String(v.description),
49+
info: String(v.info),
50+
immediate: Boolean(v.immediate),
51+
ignore_errors: Boolean(v.ignore_errors),
52+
args: String(v.args),
53+
icon: String(v.icon),
54+
params: ! Array.isArray(v.params) ? [] : v.params.map(p =>
55+
typeof p === 'string' ? p
56+
: ! is_object(p) ? null
57+
: {
58+
value: String(p.value),
59+
multiline: Boolean(p.multiline),
60+
placeholder: String(p.placeholder),
61+
readonly: Boolean(p.readonly),
62+
})
63+
.filter(is_truthy),
64+
options: ! Array.isArray(v.options) ? [] : v.options.map(o =>
65+
! is_object(o) ? null : {
66+
value: String(o.value),
67+
default_active: Boolean(o.default_active),
68+
active: Boolean(o.active),
69+
info: String(o.info),
70+
}).filter(is_truthy),
71+
}
72+
}).filter(is_truthy)
73+
},
74+
_protected: {
75+
ref: config,
76+
},
77+
}

web/src/data/store/index.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { computed, nextTick, ref, watch } from 'vue'
22
import { add_push_listener, show_information_message } from '../../bridge.js'
33
import state, { refresh_repo_states } from '../state.js'
44
import * as repo_store from './repo.js'
5+
import config from './config.js'
56

6-
export let web_phase = state('web-phase', 'initializing').ref
7+
export let web_phase = state('web-phase', 'initializing', undefined, { write_only: true }).ref
78

89
/** @type {Vue.Ref<Readonly<Vue.ShallowRef<typeof import('../../components/GitInput.vue')|null>>|null>} */
910
export let main_view_git_input_ref = ref(null)
@@ -29,14 +30,16 @@ export let _run_main_refresh = async (log_args, { fetch_stash_refs, fetch_branch
2930
let preliminary_loading = false
3031
if (web_phase.value === 'initializing')
3132
web_phase.value = 'initializing_repo'
33+
else if (web_phase.value === 'dead')
34+
throw new Error('tried refreshing when webview is dead')
3235
else if (web_phase.value !== 'initializing_repo')
3336
web_phase.value = 'refreshing'
3437
if (web_phase.value === 'initializing_repo') {
3538
repo_store._protected.unset()
3639
if (! selected_repo_path_is_valid.value)
3740
return web_phase.value = 'ready'
3841
refresh_repo_states()
39-
preliminary_loading = ! config.value['disable-preliminary-loading']
42+
preliminary_loading = ! config.get_boolean_or_undefined('disable-preliminary-loading')
4043
}
4144
await repo_store._protected.refresh(log_args, { preliminary_loading, fetch_stash_refs, fetch_branches })
4245
web_phase.value = 'ready'
@@ -59,12 +62,8 @@ export let selected_repo_path = state('selected-repo-path', '', () => {
5962
/** @type {Vue.Ref<GitAction|null>} */
6063
export let selected_git_action = ref(null)
6164

62-
// TODO: actual settings but with everything set to optional
63-
// TODO :test extension with all set to null
64-
export let config = state('config', {}).ref
65-
6665
export let vis_v_width = computed(() =>
67-
Number(config.value['branch-width']) || 10)
66+
config.get_number('branch-width') || 10)
6867
export let vis_width = state('vis-width', 130).ref
6968

7069
export let combine_branches_to_branch_name = ref('')
@@ -103,7 +102,7 @@ add_push_listener('repo-external-state-change', () => trigger_main_refresh())
103102

104103
add_push_listener('refresh-main-view', () => trigger_main_refresh())
105104

106-
watch(config, async () => {
105+
watch(config._protected.ref, async () => {
107106
web_phase.value = 'initializing_repo'
108107
trigger_main_refresh()
109108
})

web/src/data/store/repo.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { computed, ref, watch } from 'vue'
22
import { git, show_information_message } from '../../bridge.js'
33
import { parse } from '../../utils/log-parser.js'
4-
import { config } from './index.js'
54
import { _protected as search_protected } from './search'
65
import state from '../state.js'
76
import { update_commit_stats } from './commit-stats.js'
7+
import config from './config.js'
88

99
/** @type {Vue.Ref<Commit[]|null>} */
1010
export let loaded_commits = ref(null)
@@ -67,15 +67,19 @@ async function git_log(/** @type {string} */ log_args, { fetch_stash_refs = true
6767
/** @type {Awaited<ReturnType<parse>>} */
6868
let parsed = { commits: [], branches: [] }
6969
if (log_data)
70-
parsed = await parse(log_data, branch_data, stash_data, sep, config.value['curve-radius'], config.value['branch-colors'], config.value['branch-color-strategy'] === 'name-based', config.value['branch-color-custom-mapping'])
70+
parsed = await parse(log_data, branch_data, stash_data, sep,
71+
config.get_number('curve-radius'),
72+
config.get_string_array('branch-colors'),
73+
config.get_string('branch-color-strategy') === 'name-based',
74+
config.get_string_map('branch-color-custom-mapping'))
7175
return parsed
7276
}
7377
/** @param log_args {string} @param options {{ preliminary_loading?: boolean, fetch_stash_refs?: boolean, fetch_branches?: boolean }} */
7478
let refresh = async (log_args, { preliminary_loading, fetch_stash_refs, fetch_branches }) => {
7579
let preliminary_loading_promise = null
7680
if (preliminary_loading)
77-
// The "main" main log happens below, but because of the large default_log_action_n, this can take several seconds for large repos.
78-
// This below is a bit of a pre-flight request optimized for speed to show the first few commits while the rest keeps loading in the background.
81+
// The "main" main log happens below, but because of the large default_log_action_n, this can take several seconds for large repos.
82+
// This below is a bit of a pre-flight request optimized for speed to show the first few commits while the rest keeps loading in the background.
7983
preliminary_loading_promise = git_log(`${base_log_args} --author-date-order -n 100 --all`,
8084
{ fetch_stash_refs: false, fetch_branches: false }).then((parsed) =>
8185
loaded_commits.value = parsed.commits
@@ -112,7 +116,7 @@ watch(visible_commits, async () => {
112116
commit.hash && ! commit.stats)
113117
if (! visible_cp.length)
114118
return
115-
if (! config.value['disable-commit-stats'])
119+
if (! config.get_boolean_or_undefined('disable-commit-stats'))
116120
await update_commit_stats(visible_cp)
117121
})
118122

web/src/views/main-view/CommitDetails.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,10 @@
103103
<script setup>
104104
import { ref, computed, watchEffect } from 'vue'
105105
import { git } from '../../bridge.js'
106-
import { config, show_branch } from '../../data/store/index.js'
106+
import { show_branch } from '../../data/store/index.js'
107107
import { commit_actions as commit_actions_, stash_actions as stash_actions_, branch_actions as branch_actions_, tag_actions as tag_actions_ } from '../../data/store/actions.js'
108108
import { filtered_commits, loaded_commits } from '../../data/store/repo.js'
109+
import config from '../../data/store/config.js'
109110
110111
let props = defineProps({
111112
commit: {
@@ -117,7 +118,7 @@ let props = defineProps({
117118
defineEmits(['hash_clicked'])
118119
119120
let details_panel_position = computed(() =>
120-
config.value['details-panel-position'])
121+
config.get_string('details-panel-position'))
121122
122123
let branch_tips = computed(() =>
123124
props.commit.refs.filter(is_branch))
@@ -152,7 +153,7 @@ let tag_actions = computed(() => (/** @type {string} */ tag_name) =>
152153
tag_actions_(tag_name).value)
153154
154155
let config_show_buttons = computed(() =>
155-
! config.value['hide-sidebar-buttons'])
156+
! config.get_boolean_or_undefined('hide-sidebar-buttons'))
156157
157158
let index_in_filtered_commits = computed(() =>
158159
props.commit ? filtered_commits.value.indexOf(props.commit) : -1)

0 commit comments

Comments
 (0)