git-stack-cli 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/dist/js/index.js +639 -528
- package/package.json +1 -1
- package/scripts/bun-build.ts +5 -0
- package/src/app/App.tsx +11 -5
- package/src/app/AutoUpdate.tsx +72 -54
- package/src/app/GatherMetadata.tsx +2 -24
- package/src/app/Main.tsx +0 -4
- package/src/app/MultiSelect.tsx +1 -8
- package/src/app/RequireBranch.tsx +67 -0
- package/src/app/SelectCommitRanges.tsx +9 -17
- package/src/app/Status.tsx +1 -5
- package/src/app/Store.tsx +2 -1
- package/src/command.ts +68 -50
- package/src/commands/Config.tsx +106 -0
- package/src/commands/Rebase.tsx +50 -36
- package/src/components/ColorTest.tsx +49 -0
- package/src/core/cli.ts +2 -0
- package/src/core/colors.ts +4 -2
- package/src/index.tsx +2 -2
- package/src/types/global.d.ts +1 -1
- package/src/app/PreSelectCommitRanges.tsx +0 -29
- package/src/core/gs_short_id.ts +0 -5
package/src/command.ts
CHANGED
|
@@ -1,54 +1,78 @@
|
|
|
1
1
|
import yargs from "yargs";
|
|
2
2
|
import { hideBin } from "yargs/helpers";
|
|
3
3
|
|
|
4
|
-
import type { Options, InferredOptionTypes, Arguments } from "yargs";
|
|
4
|
+
import type { Options, InferredOptionTypes, Arguments, ParserConfigurationOptions } from "yargs";
|
|
5
5
|
|
|
6
6
|
export type Argv = Arguments & TGlobalOptions & TFixupOptions & TDefaultOptions;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type CommandOptions = {
|
|
9
|
+
env_config?: Partial<Argv>;
|
|
10
|
+
parserConfiguration?: Partial<ParserConfigurationOptions>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function command(argv: string[], options: CommandOptions = {}) {
|
|
9
14
|
// https://yargs.js.org/docs/#api-reference-optionkey-opt
|
|
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
|
-
|
|
15
|
+
let builder = yargs(hideBin(argv));
|
|
16
|
+
|
|
17
|
+
if (options.parserConfiguration) {
|
|
18
|
+
builder = builder.parserConfiguration(options.parserConfiguration);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// apply overrides from config
|
|
22
|
+
// higher precedence than defaults, but lower precendence than cli flags
|
|
23
|
+
// perfect since that's what we want, prefer config only if not explicitly set on cli
|
|
24
|
+
if (options.env_config) {
|
|
25
|
+
builder = builder.config(options.env_config);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsed = await builder
|
|
29
|
+
.scriptName("git stack")
|
|
30
|
+
.usage("Usage: git stack [command] [options]")
|
|
31
|
+
|
|
32
|
+
.command("$0", "Sync commit ranges to Github", (yargs) => yargs.options(DefaultOptions))
|
|
33
|
+
|
|
34
|
+
.command("fixup [commit]", "Amend staged changes to a specific commit in history", (yargs) =>
|
|
35
|
+
yargs.positional("commit", FixupOptions.commit),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
.command(
|
|
39
|
+
"log [args...]",
|
|
40
|
+
"Print an abbreviated log with numbered commits, useful for git stack fixup",
|
|
41
|
+
(yargs) => yargs.strict(false),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
.command(
|
|
45
|
+
"rebase",
|
|
46
|
+
"Update local branch via rebase with latest changes from origin master branch",
|
|
47
|
+
(yargs) => yargs,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
.command(
|
|
51
|
+
["update", "upgrade"],
|
|
52
|
+
"Check and install the latest version of git stack",
|
|
53
|
+
(yargs) => yargs,
|
|
54
|
+
)
|
|
55
|
+
.command(
|
|
56
|
+
"config",
|
|
57
|
+
"Generate a one-time configuration json based on the passed arguments",
|
|
58
|
+
(yargs) => yargs.options(DefaultOptions),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
.option("verbose", GlobalOptions.verbose)
|
|
62
|
+
|
|
63
|
+
// yargs default wraps to 80 columns
|
|
64
|
+
// passing null will wrap to terminal width
|
|
65
|
+
// value below if what seems to look decent
|
|
66
|
+
.wrap(123)
|
|
67
|
+
|
|
68
|
+
// disallow unknown options
|
|
69
|
+
.strict()
|
|
70
|
+
.version(process.env.CLI_VERSION || "unknown")
|
|
71
|
+
.showHidden("show-hidden", "Show hidden options via `git stack help --show-hidden`")
|
|
72
|
+
.help("help", "Show usage via `git stack help`");
|
|
73
|
+
|
|
74
|
+
const result = parsed.argv as unknown as Argv;
|
|
75
|
+
return result;
|
|
52
76
|
}
|
|
53
77
|
|
|
54
78
|
const GlobalOptions = {
|
|
@@ -109,12 +133,6 @@ const DefaultOptions = {
|
|
|
109
133
|
description: "Open all PRs as drafts",
|
|
110
134
|
},
|
|
111
135
|
|
|
112
|
-
"branch-prefix": {
|
|
113
|
-
type: "string",
|
|
114
|
-
default: "",
|
|
115
|
-
description: "Prefix for generated branch names, e.g. dev/magus/",
|
|
116
|
-
},
|
|
117
|
-
|
|
118
136
|
"revise-sign": {
|
|
119
137
|
type: "boolean",
|
|
120
138
|
default: true,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import * as Ink from "ink-cjs";
|
|
4
|
+
|
|
5
|
+
import { Await } from "~/app/Await";
|
|
6
|
+
import { FormatText } from "~/app/FormatText";
|
|
7
|
+
import { Store } from "~/app/Store";
|
|
8
|
+
import { command } from "~/command";
|
|
9
|
+
import { colors } from "~/core/colors";
|
|
10
|
+
import { invariant } from "~/core/invariant";
|
|
11
|
+
|
|
12
|
+
export function Config() {
|
|
13
|
+
return <Await fallback={null} function={run} />;
|
|
14
|
+
|
|
15
|
+
async function run() {
|
|
16
|
+
const state = Store.getState();
|
|
17
|
+
const actions = state.actions;
|
|
18
|
+
|
|
19
|
+
const config = await get_explicit_args();
|
|
20
|
+
const config_json = JSON.stringify(config).replace(/"/g, '\\"');
|
|
21
|
+
|
|
22
|
+
actions.output(
|
|
23
|
+
<Ink.Box flexDirection="column" gap={1} paddingTop={1}>
|
|
24
|
+
<Ink.Text></Ink.Text>
|
|
25
|
+
|
|
26
|
+
<FormatText
|
|
27
|
+
message="Add the line below to your shell rc file ({zshrc}, {bashrc}, etc.)"
|
|
28
|
+
values={{
|
|
29
|
+
zshrc: <Ink.Text color={colors.gray}>.zshrc</Ink.Text>,
|
|
30
|
+
bashrc: <Ink.Text color={colors.gray}>.bashrc</Ink.Text>,
|
|
31
|
+
}}
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<FormatText
|
|
35
|
+
message={`{export} {ENV_VAR}{e}{q}{config_json}{q}`}
|
|
36
|
+
values={{
|
|
37
|
+
export: <Ink.Text color={colors.purple}>export</Ink.Text>,
|
|
38
|
+
ENV_VAR: <Ink.Text color={colors.yellow}>{ENV_VAR}</Ink.Text>,
|
|
39
|
+
e: <Ink.Text color={colors.purple}>{"="}</Ink.Text>,
|
|
40
|
+
q: <Ink.Text color={colors.white}>{'"'}</Ink.Text>,
|
|
41
|
+
config_json: <Ink.Text color={colors.green}>{config_json}</Ink.Text>,
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
</Ink.Box>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
actions.exit(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function argv_with_config_from_env() {
|
|
52
|
+
if (!process.env.GIT_STACK_CONFIG) {
|
|
53
|
+
return await command(process.argv);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const env_config = parse_env_config();
|
|
57
|
+
return await command(process.argv, { env_config });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parse_env_config() {
|
|
61
|
+
const GIT_STACK_CONFIG = process.env.GIT_STACK_CONFIG;
|
|
62
|
+
invariant(GIT_STACK_CONFIG, "GIT_STACK_CONFIG must exist");
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const env_config = JSON.parse(GIT_STACK_CONFIG);
|
|
66
|
+
return env_config;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.error(`ERROR GIT_STACK_CONFIG=${GIT_STACK_CONFIG}`);
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.error("ERROR GIT_STACK_CONFIG environment variable is not valid JSON");
|
|
72
|
+
process.exit(18);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function get_explicit_args() {
|
|
77
|
+
const default_argv = await command(["git", "stack"], COMMAND_OPTIONS);
|
|
78
|
+
const state_argv = await command(process.argv, COMMAND_OPTIONS);
|
|
79
|
+
|
|
80
|
+
const config: Record<string, any> = {};
|
|
81
|
+
|
|
82
|
+
// find delta between default_argv and argv
|
|
83
|
+
for (const key of Object.keys(state_argv)) {
|
|
84
|
+
if (key === "_" || key === "$0") continue;
|
|
85
|
+
|
|
86
|
+
const state_value = state_argv[key];
|
|
87
|
+
const default_value = default_argv[key];
|
|
88
|
+
const is_set = default_value !== state_value;
|
|
89
|
+
if (is_set) {
|
|
90
|
+
config[key] = state_value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ENV_VAR = "GIT_STACK_CONFIG";
|
|
98
|
+
|
|
99
|
+
type CommandOptions = NonNullable<Parameters<typeof command>[1]>;
|
|
100
|
+
|
|
101
|
+
const COMMAND_OPTIONS = {
|
|
102
|
+
parserConfiguration: {
|
|
103
|
+
// Should aliases be removed before returning results? Default is `false`
|
|
104
|
+
"strip-aliased": true,
|
|
105
|
+
},
|
|
106
|
+
} satisfies CommandOptions;
|
package/src/commands/Rebase.tsx
CHANGED
|
@@ -22,7 +22,7 @@ type Props = {
|
|
|
22
22
|
export function Rebase(props: Props) {
|
|
23
23
|
return (
|
|
24
24
|
<Await
|
|
25
|
-
fallback={<Ink.Text color={colors.yellow}>Rebasing
|
|
25
|
+
fallback={<Ink.Text color={colors.yellow}>Rebasing…</Ink.Text>}
|
|
26
26
|
function={() => Rebase.run(props)}
|
|
27
27
|
/>
|
|
28
28
|
);
|
|
@@ -48,6 +48,7 @@ Rebase.run = async function run(props: Props) {
|
|
|
48
48
|
return 19;
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
const master_branch_name = master_branch.replace(/^origin\//, "");
|
|
51
52
|
const temp_branch_name = `${branch_name}_${short_id()}`;
|
|
52
53
|
|
|
53
54
|
try {
|
|
@@ -58,9 +59,56 @@ Rebase.run = async function run(props: Props) {
|
|
|
58
59
|
await cli(`pwd`);
|
|
59
60
|
|
|
60
61
|
// fetch origin master branch for latest sha
|
|
61
|
-
const master_branch_name = master_branch.replace(/^origin\//, "");
|
|
62
62
|
await cli(`git fetch --no-tags -v origin ${master_branch_name}`);
|
|
63
63
|
|
|
64
|
+
if (branch_name === master_branch_name) {
|
|
65
|
+
await rebase_master();
|
|
66
|
+
} else {
|
|
67
|
+
await rebase_branch();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
actions.unregister_abort_handler();
|
|
71
|
+
} catch (err) {
|
|
72
|
+
actions.error("Unable to rebase.");
|
|
73
|
+
|
|
74
|
+
if (err instanceof Error) {
|
|
75
|
+
actions.error(err.message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
actions.exit(20);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const next_commit_range = await CommitMetadata.range();
|
|
82
|
+
|
|
83
|
+
actions.output(
|
|
84
|
+
<FormatText
|
|
85
|
+
wrapper={<Ink.Text color={colors.green} />}
|
|
86
|
+
message="✅ {branch_name} in sync with {origin_branch}"
|
|
87
|
+
values={{
|
|
88
|
+
branch_name: <Brackets>{branch_name}</Brackets>,
|
|
89
|
+
origin_branch: <Brackets>{master_branch}</Brackets>,
|
|
90
|
+
}}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
actions.set((state) => {
|
|
95
|
+
state.commit_range = next_commit_range;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (props.onComplete) {
|
|
99
|
+
props.onComplete();
|
|
100
|
+
} else {
|
|
101
|
+
actions.output(<Status />);
|
|
102
|
+
actions.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function rebase_master() {
|
|
106
|
+
await cli(`git switch -C "${master_branch_name}" "${master_branch}"`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function rebase_branch() {
|
|
110
|
+
invariant(commit_range, "commit_range must exist");
|
|
111
|
+
|
|
64
112
|
const master_sha = (await cli(`git rev-parse ${master_branch}`)).stdout;
|
|
65
113
|
const rebase_merge_base = master_sha;
|
|
66
114
|
|
|
@@ -120,40 +168,6 @@ Rebase.run = async function run(props: Props) {
|
|
|
120
168
|
await cli(`git branch -f ${branch_name} ${temp_branch_name}`);
|
|
121
169
|
|
|
122
170
|
restore_git();
|
|
123
|
-
|
|
124
|
-
actions.unregister_abort_handler();
|
|
125
|
-
} catch (err) {
|
|
126
|
-
actions.error("Unable to rebase.");
|
|
127
|
-
|
|
128
|
-
if (err instanceof Error) {
|
|
129
|
-
actions.error(err.message);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
actions.exit(20);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const next_commit_range = await CommitMetadata.range();
|
|
136
|
-
|
|
137
|
-
actions.output(
|
|
138
|
-
<FormatText
|
|
139
|
-
wrapper={<Ink.Text color={colors.green} />}
|
|
140
|
-
message="✅ {branch_name} in sync with {origin_branch}"
|
|
141
|
-
values={{
|
|
142
|
-
branch_name: <Brackets>{branch_name}</Brackets>,
|
|
143
|
-
origin_branch: <Brackets>{master_branch}</Brackets>,
|
|
144
|
-
}}
|
|
145
|
-
/>,
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
actions.set((state) => {
|
|
149
|
-
state.commit_range = next_commit_range;
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
if (props.onComplete) {
|
|
153
|
-
props.onComplete();
|
|
154
|
-
} else {
|
|
155
|
-
actions.output(<Status />);
|
|
156
|
-
actions.exit(0);
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
// cleanup git operations if cancelled during manual rebase
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { colors } from "~/core/colors";
|
|
4
|
+
|
|
5
|
+
type RenderOptions = {
|
|
6
|
+
color: string;
|
|
7
|
+
name: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
children: (render_options: RenderOptions) => React.ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function ColorTest(props: Props) {
|
|
15
|
+
return (
|
|
16
|
+
<React.Fragment>
|
|
17
|
+
{Object.entries(colors).map(([key, color]) => {
|
|
18
|
+
const name = `colors:${key}`;
|
|
19
|
+
return props.children({ color, name });
|
|
20
|
+
})}
|
|
21
|
+
|
|
22
|
+
{INK_COLORS.map((color) => {
|
|
23
|
+
const name = `ink:${color}`;
|
|
24
|
+
return props.children({ color, name });
|
|
25
|
+
})}
|
|
26
|
+
</React.Fragment>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ForegroundColor
|
|
31
|
+
// https://github.com/magus/git-stack-cli/blob/master/node_modules/.pnpm/chalk@5.3.0/node_modules/chalk/source/vendor/ansi-styles/index.d.ts#L75
|
|
32
|
+
const INK_COLORS = [
|
|
33
|
+
"black",
|
|
34
|
+
"red",
|
|
35
|
+
"green",
|
|
36
|
+
"yellow",
|
|
37
|
+
"blue",
|
|
38
|
+
"cyan",
|
|
39
|
+
"magenta",
|
|
40
|
+
"white",
|
|
41
|
+
"blackBright",
|
|
42
|
+
"redBright",
|
|
43
|
+
"greenBright",
|
|
44
|
+
"yellowBright",
|
|
45
|
+
"blueBright",
|
|
46
|
+
"cyanBright",
|
|
47
|
+
"magentaBright",
|
|
48
|
+
"whiteBright",
|
|
49
|
+
];
|
package/src/core/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ type SpawnOptions = Parameters<typeof child.spawn>[2];
|
|
|
7
7
|
|
|
8
8
|
type Options = SpawnOptions & {
|
|
9
9
|
ignoreExitCode?: boolean;
|
|
10
|
+
onOutput?: (data: string) => void;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
type Return = {
|
|
@@ -51,6 +52,7 @@ export async function cli(
|
|
|
51
52
|
function write_output(value: string) {
|
|
52
53
|
output += value;
|
|
53
54
|
state.actions.debug(value, id);
|
|
55
|
+
options.onOutput?.(value);
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
childProcess.stdout?.on("data", (data: Buffer) => {
|
package/src/core/colors.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ink uses chalk internally
|
|
2
2
|
// https://github.com/vadimdemedes/ink#color
|
|
3
3
|
|
|
4
|
-
export const colors = {
|
|
4
|
+
export const colors = Object.freeze({
|
|
5
5
|
red: "rgb(248, 81, 73)",
|
|
6
6
|
// red-emphasis rgb(218, 54, 51)
|
|
7
7
|
|
|
@@ -20,4 +20,6 @@ export const colors = {
|
|
|
20
20
|
gray: "rgb(110, 118, 129)",
|
|
21
21
|
|
|
22
22
|
lightGray: "rgb(125, 133, 144)",
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
white: "whiteBright",
|
|
25
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -11,13 +11,13 @@ import * as Ink from "ink-cjs";
|
|
|
11
11
|
|
|
12
12
|
import { App } from "~/app/App";
|
|
13
13
|
import { Store } from "~/app/Store";
|
|
14
|
-
import {
|
|
14
|
+
import { argv_with_config_from_env } from "~/commands/Config";
|
|
15
15
|
import { get_tmp_dir } from "~/core/get_tmp_dir";
|
|
16
16
|
import { pretty_json } from "~/core/pretty_json";
|
|
17
17
|
|
|
18
18
|
(async function main() {
|
|
19
19
|
try {
|
|
20
|
-
const argv = await
|
|
20
|
+
const argv = await argv_with_config_from_env();
|
|
21
21
|
|
|
22
22
|
// required to get bun working with ink
|
|
23
23
|
// https://github.com/oven-sh/bun/issues/6862#issuecomment-2429444852
|
package/src/types/global.d.ts
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import { Store } from "~/app/Store";
|
|
4
|
-
import { YesNoPrompt } from "~/app/YesNoPrompt";
|
|
5
|
-
|
|
6
|
-
export function PreSelectCommitRanges() {
|
|
7
|
-
const actions = Store.useActions();
|
|
8
|
-
const argv = Store.useState((state) => state.argv);
|
|
9
|
-
|
|
10
|
-
React.useEffect(() => {
|
|
11
|
-
if (argv.force) {
|
|
12
|
-
Store.setState((state) => {
|
|
13
|
-
state.step = "select-commit-ranges";
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
}, [argv]);
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<YesNoPrompt
|
|
20
|
-
message="Some commits are new or outdated, would you like to select new commit ranges?"
|
|
21
|
-
onYes={() => {
|
|
22
|
-
actions.set((state) => {
|
|
23
|
-
state.step = "select-commit-ranges";
|
|
24
|
-
});
|
|
25
|
-
}}
|
|
26
|
-
onNo={() => actions.exit(0)}
|
|
27
|
-
/>
|
|
28
|
-
);
|
|
29
|
-
}
|