kup-cli 0.1.3 → 0.2.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 +85 -67
- package/README.zh.md +165 -0
- package/bin/cli.js +8 -1
- package/lib/file.js +10 -0
- package/lib/main.js +58 -18
- package/lib/meta.js +86 -9
- package/lib/parse.js +6 -4
- package/lib/sync.js +97 -9
- package/lib/validate.js +22 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
English | [中文](README.zh.md)
|
|
2
|
+
|
|
1
3
|
# Kup
|
|
2
4
|
|
|
3
5
|
<img src="https://user-images.githubusercontent.com/1231359/171321963-e8e73bdf-f9c8-435b-9707-318e92f1805a.png" width="150" height="80" align="right" alt="Kup logo">
|
|
@@ -6,135 +8,151 @@
|
|
|
6
8
|
>
|
|
7
9
|
> 这款命令行工具可以把本地的 Markdown 文件同步到 GitHub issue。
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Who Is It For
|
|
10
12
|
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Bloggers who publish with GitHub issues and want to edit drafts locally, then publish new posts or update existing ones at any time.
|
|
14
|
+
* Open-source authors who publish documentation through GitHub issues and want to sync docs from the repository to specific issues.
|
|
15
|
+
* Anyone else who needs to update GitHub issues frequently.
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
##
|
|
18
|
+
## Installation
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
Install it globally so you can call it from the command line at any time:
|
|
19
21
|
|
|
20
22
|
```sh
|
|
21
|
-
npm
|
|
23
|
+
npm install -g kup-cli
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
(When a new version of Kup is released, run the same command again to upgrade.)
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
##
|
|
29
|
+
## Usage
|
|
28
30
|
|
|
29
|
-
###
|
|
31
|
+
### Publish a New Issue
|
|
30
32
|
|
|
31
33
|
```sh
|
|
32
34
|
kup ./path/to/file.md --repo foo/bar
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
Kup
|
|
37
|
+
Kup will publish the content of `file.md` as a new issue in the `foo/bar` repository, and report the new issue number after it succeeds.
|
|
36
38
|
|
|
37
|
-
###
|
|
39
|
+
### Update an Existing Issue
|
|
38
40
|
|
|
39
41
|
```sh
|
|
40
42
|
kup ./path/to/file.md --repo foo/bar --id 123
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
Kup
|
|
45
|
+
Kup will update issue `123` in the `foo/bar` repository with the content of `file.md`.
|
|
46
|
+
|
|
47
|
+
### Dump an Existing Issue
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
kup --dump --repo foo/bar --id 123
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Kup will dump issue `123` from the `foo/bar` repository to `123.md` in the current directory.
|
|
54
|
+
|
|
55
|
+
If you want to specify the output file, you can also use:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
kup ./path/to/file.md --dump --repo foo/bar --id 123
|
|
59
|
+
```
|
|
44
60
|
|
|
45
|
-
###
|
|
61
|
+
### Preparation
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
Updating GitHub issues requires authentication, so you need to provide a GitHub token for Kup to call the GitHub API.
|
|
48
64
|
|
|
49
|
-
1.
|
|
65
|
+
1. Create a new token on GitHub's "[Personal access tokens](https://github.com/settings/tokens)" page, and make sure the `repo` scope is selected. (See [this document](https://github.com/cssmagic/kup/issues/17) for details.)
|
|
50
66
|
|
|
51
|
-
2.
|
|
67
|
+
2. Put the token into an environment variable:
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
```sh
|
|
70
|
+
export GITHUB_TOKEN=ghp_**********
|
|
71
|
+
```
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
If Kup cannot read a token from the environment, it will prompt you for one in the terminal.
|
|
58
74
|
|
|
59
75
|
|
|
60
|
-
##
|
|
76
|
+
## Command-Line Options
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
Option | Short | Value Type | Description | Notes
|
|
63
79
|
---|---|---|---|---
|
|
64
|
-
`--repo` | `-r` |
|
|
65
|
-
`--id` | `-i` |
|
|
66
|
-
`--
|
|
67
|
-
`--
|
|
80
|
+
`--repo` | `-r` | string | Specify the GitHub repository
|
|
81
|
+
`--id` | `-i` | integer | Specify the issue number | <ul><li>Providing a number means updating an existing issue<li>Omitting it means publishing a new issue</ul>
|
|
82
|
+
`--dump` | `-d` | - | Enable dump mode, save a GitHub issue as a local Markdown file
|
|
83
|
+
`--version` | `-v` | - | Show the version number
|
|
84
|
+
`--help` | `-h` | - | Show help information
|
|
68
85
|
|
|
69
86
|
|
|
70
|
-
##
|
|
87
|
+
## Advanced
|
|
71
88
|
|
|
72
|
-
###
|
|
89
|
+
### How can I specify the `repo` option more conveniently?
|
|
73
90
|
|
|
74
|
-
|
|
91
|
+
Kup can determine the `repo` option in the following order of priority:
|
|
75
92
|
|
|
76
|
-
1.
|
|
77
|
-
1. Markdown
|
|
78
|
-
1.
|
|
79
|
-
1.
|
|
80
|
-
1.
|
|
93
|
+
1. The `--repo` option passed on the command line.
|
|
94
|
+
1. The `repo` field in the Markdown file's [metadata](https://github.com/cssmagic/kup/issues/1).
|
|
95
|
+
1. The `kup.repo` field in the current project's `package.json`. Kup starts from the Markdown file's directory and searches upward for a `package.json` file.
|
|
96
|
+
1. If `kup.repo` is not present in `package.json`, Kup will try to infer the repository name from the `repository` field and ask for confirmation before using it.
|
|
97
|
+
1. If that still fails, Kup will continue searching for `.git/config` in the current or parent directories, then try to infer the repository name from the `url` field of `remote "origin"` and ask for confirmation before using it.
|
|
81
98
|
|
|
82
|
-
|
|
99
|
+
If the entire project syncs to the same repository, it is usually best to configure `kup.repo` in `package.json`.
|
|
83
100
|
|
|
84
|
-
###
|
|
101
|
+
### How can I specify the `id` option more conveniently?
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
Kup can determine the `id` option in the following order of priority:
|
|
87
104
|
|
|
88
|
-
1.
|
|
89
|
-
1.
|
|
105
|
+
1. The `--id` option passed on the command line.
|
|
106
|
+
1. The `id` field in the Markdown file's metadata.
|
|
90
107
|
|
|
91
|
-
|
|
108
|
+
After a file is successfully published as an issue, Kup writes the `id` back to the file’s metadata (with user confirmation before writing).
|
|
92
109
|
|
|
93
|
-
###
|
|
110
|
+
### How is the issue title determined?
|
|
94
111
|
|
|
95
|
-
Kup
|
|
112
|
+
Kup determines the issue title using the following hints, in descending priority:
|
|
96
113
|
|
|
97
|
-
1.
|
|
98
|
-
1. Markdown
|
|
114
|
+
1. The `title` field in the Markdown file's metadata.
|
|
115
|
+
1. If the first Markdown block is an H1 heading (in `# Title` form), Kup uses its text as the title. In this case, that H1 heading is excluded from the synced content.
|
|
99
116
|
|
|
100
|
-
|
|
117
|
+
What happens if Kup still cannot determine the issue title?
|
|
101
118
|
|
|
102
|
-
*
|
|
103
|
-
*
|
|
119
|
+
* When publishing a new issue, Kup generates a title automatically.
|
|
120
|
+
* When updating an existing issue, Kup ignores the title, which means the existing title will not be changed.
|
|
104
121
|
|
|
105
|
-
###
|
|
122
|
+
### How do I assign labels to an issue?
|
|
106
123
|
|
|
107
|
-
|
|
124
|
+
Whether you are blogging or publishing regular issues, labels are often useful, so Kup supports them as well.
|
|
108
125
|
|
|
109
|
-
|
|
126
|
+
Add a `tags` field to the Markdown file's metadata and specify one or more labels. These labels do not need to exist in the GitHub repository beforehand. If a label does not already exist, it will be created automatically when the issue is published.
|
|
110
127
|
|
|
111
|
-
|
|
128
|
+
What happens if there is no `tags` field in the metadata?
|
|
112
129
|
|
|
113
|
-
*
|
|
114
|
-
*
|
|
130
|
+
* When publishing a new issue, Kup will not assign any labels.
|
|
131
|
+
* When updating an existing issue, Kup ignores labels, which means the existing labels will not be changed.
|
|
115
132
|
|
|
116
|
-
|
|
133
|
+
When updating an existing issue, if the labels specified in the metadata differ from the issue's current labels, the new set completely replaces the old one.
|
|
117
134
|
|
|
118
|
-
##
|
|
135
|
+
## Documentation
|
|
119
136
|
|
|
120
|
-
* [Markdown
|
|
121
|
-
* [
|
|
137
|
+
* [Metadata examples for Markdown files](https://github.com/cssmagic/kup/issues/1)
|
|
138
|
+
* [How to generate an appropriate GitHub token for Kup](https://github.com/cssmagic/kup/issues/17)
|
|
122
139
|
|
|
123
140
|
|
|
124
|
-
##
|
|
141
|
+
## Other
|
|
125
142
|
|
|
126
|
-
###
|
|
143
|
+
### Roadmap
|
|
127
144
|
|
|
128
|
-
|
|
145
|
+
* See [the issue list for this project](https://github.com/cssmagic/kup/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) for current plans.
|
|
146
|
+
* Feature requests and RFC discussions are welcome.
|
|
129
147
|
|
|
130
|
-
###
|
|
148
|
+
### About the Name
|
|
131
149
|
|
|
132
|
-
* Kup
|
|
133
|
-
* Kup
|
|
150
|
+
* Kup comes from the English word "pickup", suggesting lightweight transport.
|
|
151
|
+
* Kup is also a character in *Transformers*.
|
|
134
152
|
|
|
135
|
-
###
|
|
153
|
+
### About the Logo
|
|
136
154
|
|
|
137
|
-
*
|
|
155
|
+
* The logo was created by [Fasil](https://freeicons.io/profile/722) and provided for free via [freeicons.io](https://freeicons.io/icon/e-commerce-icons/pickup-truck-icon-26893).
|
|
138
156
|
|
|
139
157
|
***
|
|
140
158
|
|
|
@@ -142,6 +160,6 @@ Kup 通过以下线索来确定 issue 的标题,优先级递减:
|
|
|
142
160
|
|
|
143
161
|
> Any code contributed to this project is considered authorized for commercial use by the project authors and their affiliated companies and distributed under this project's license.
|
|
144
162
|
>
|
|
145
|
-
>
|
|
163
|
+
> 任何贡献到本项目的代码,均视为授权本项目作者及其关联公司用于商业用途,并可按本项目协议进行分发。
|
|
146
164
|
|
|
147
165
|
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
[English](README.md) | 中文
|
|
2
|
+
|
|
3
|
+
# Kup
|
|
4
|
+
|
|
5
|
+
<img src="https://user-images.githubusercontent.com/1231359/171321963-e8e73bdf-f9c8-435b-9707-318e92f1805a.png" width="150" height="80" align="right" alt="Kup logo">
|
|
6
|
+
|
|
7
|
+
> A CLI tool to sync local Markdown files to GitHub issues.
|
|
8
|
+
>
|
|
9
|
+
> 这款命令行工具可以把本地的 Markdown 文件同步到 GitHub issue。
|
|
10
|
+
|
|
11
|
+
## 谁需要它
|
|
12
|
+
|
|
13
|
+
* 利用 GitHub issue 写博客的作者们,可以方便地在本地编辑原稿,并随时发布新博文或更新已有博文。
|
|
14
|
+
* 通过 GitHub issue 发布文档的开源软件作者们,可以把代码仓库中的文档更新到指定 issue。
|
|
15
|
+
* 其它需要频繁更新 GitHub issue 的人。
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
需要全局安装,以便随时在命令行调用:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install -g kup-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
(当 Kup 发布新版时,可以再次运行这行命令升级已安装的版本。)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## 使用方法
|
|
30
|
+
|
|
31
|
+
### 发布新 issue
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
kup ./path/to/file.md --repo foo/bar
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Kup 会把 `file.md` 文件的内容发布为 `foo/bar` 仓库的一个新 issue,发布成功后会告知新 issue 的编号。
|
|
38
|
+
|
|
39
|
+
### 更新已有 issue
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
kup ./path/to/file.md --repo foo/bar --id 123
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Kup 会把 `file.md` 文件的内容更新到 `foo/bar` 仓库的编号为 `123` 的 issue。
|
|
46
|
+
|
|
47
|
+
### 采集已有 issue
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
kup --dump --repo foo/bar --id 123
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Kup 会把 `foo/bar` 仓库的编号为 `123` 的 issue 采集为当前目录下的 `123.md` 文件。
|
|
54
|
+
|
|
55
|
+
如果你想指定输出文件,也可以这样使用:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
kup ./path/to/file.md --dump --repo foo/bar --id 123
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 准备工作
|
|
62
|
+
|
|
63
|
+
操作 GitHub issue 是需要权限认证的,因此你需要向 Kup 提供 GitHub token,以便 Kup 调用 GitHub API。
|
|
64
|
+
|
|
65
|
+
1. 在 GitHub 的 “[Personal access tokens](https://github.com/settings/tokens)” 页面生成一个新 token,权限范围需要选中 “repo”。(详细说明参见 [这篇文档](https://github.com/cssmagic/kup/issues/17)。)
|
|
66
|
+
|
|
67
|
+
2. 把获取到的 token 写入环境变量:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
export GITHUB_TOKEN=ghp_**********
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
如果 Kup 未能从环境变量获取 token,会在命令行向你询问。
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## 命令行参数
|
|
77
|
+
|
|
78
|
+
参数 | 短名 | 值类型 | 含义 | 备注
|
|
79
|
+
---|---|---|---|---
|
|
80
|
+
`--repo` | `-r` | 字符串 | 指定 GitHub 仓库
|
|
81
|
+
`--id` | `-i` | 整数 | 指定 issue 的编号 | <ul><li>指定编号表示更新已有 issue<li>未指定编号则表示发布新 issue</ul>
|
|
82
|
+
`--dump` | `-d` | - | 激活采集模式,把 GitHub issue 保存为本地 Markdown 文件
|
|
83
|
+
`--version` | `-v` | - | 显示版本号
|
|
84
|
+
`--help` | `-h` | - | 显示帮助信息
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## 进阶
|
|
88
|
+
|
|
89
|
+
### 如何更方便地指定 `repo` 参数?
|
|
90
|
+
|
|
91
|
+
有以下方式可以指定 `repo` 参数,优先级递减:
|
|
92
|
+
|
|
93
|
+
1. 调用命令行时指定的 `--repo` 参数。
|
|
94
|
+
1. Markdown 文件内的 [元数据](https://github.com/cssmagic/kup/issues/1) 的 `repo` 字段。
|
|
95
|
+
1. 当前项目的 `package.json` 文件内的 `kup.repo` 字段。Kup 会以 Markdown 文件所在目录为起点逐级向上寻找 `package.json` 文件。
|
|
96
|
+
1. 如果 `package.json` 文件内没有 `kup.repo` 字段,则 Kup 会尝试根据 `repository` 字段来猜测仓库名(在使用前会向用户确认)。
|
|
97
|
+
1. 如果以上方式都没有成功,则 Kup 会继续查找同级或上级目录中的 `.git/config` 文件,并尝试根据其中 `remote "origin"` 的 `url` 字段来猜测仓库名(在使用前会向用户确认)。
|
|
98
|
+
|
|
99
|
+
如果整个项目的同步目标都是同一个仓库,通常建议在 `package.json` 里统一配置 `kup.repo`。
|
|
100
|
+
|
|
101
|
+
### 如何更方便地指定 `id` 参数?
|
|
102
|
+
|
|
103
|
+
有以下方式可以指定 `id` 参数,优先级递减:
|
|
104
|
+
|
|
105
|
+
1. 调用命令行时指定的 `--id` 参数。
|
|
106
|
+
1. Markdown 文件内的元数据的 `id` 字段。
|
|
107
|
+
|
|
108
|
+
当一个文件成功发布为 issue 后,Kup 会把 `id` 写回文件的元数据中(在写入前会向用户确认)。
|
|
109
|
+
|
|
110
|
+
### Issue 的标题是如何确定的?
|
|
111
|
+
|
|
112
|
+
Kup 会通过以下线索来确定 issue 的标题,优先级递减:
|
|
113
|
+
|
|
114
|
+
1. Markdown 文件内的元数据的 `title` 字段。
|
|
115
|
+
1. Markdown 正文的第一个标记如果是一级标题(`# Title` 格式),则取它的内容(在这种情况下,这个一级标题在同步时会从内容中排除)。
|
|
116
|
+
|
|
117
|
+
如果通过以上方式无法确定 issue 标题,Kup 会怎么处理?
|
|
118
|
+
|
|
119
|
+
* 如果是在发布新 issue,则 Kup 会自己生成一个标题。
|
|
120
|
+
* 如果是在更新已有 issue,则 Kup 会忽略标题(也就是说,不会修改已有标题)。
|
|
121
|
+
|
|
122
|
+
### 如何为 issue 指定 label?
|
|
123
|
+
|
|
124
|
+
不论是在写博客,还是在发表 issue,你常常都会有打标签的需求。因此 Kup 也实现了这个功能。
|
|
125
|
+
|
|
126
|
+
你需要在 Markdown 文件的元数据中添加 `tags` 字段,指定一个或多个 label。这些 label 不需要事先在 GitHub 仓库里创建好——如果你指定了不存在的 label,会在发布 issue 时自动创建。
|
|
127
|
+
|
|
128
|
+
如果元数据中没有 `tags` 字段,Kup 会怎么处理?
|
|
129
|
+
|
|
130
|
+
* 如果是在发布新 issue,则 Kup 不会为 issue 设置任何 label。
|
|
131
|
+
* 如果是在更新已有 issue 时,Kup 会忽略标签(也就是说,不会修改已有标签)。
|
|
132
|
+
|
|
133
|
+
在更新已有 issue 时,如果元数据指定的标签与 issue 现有标签不一致,则前者会完全替代后者。
|
|
134
|
+
|
|
135
|
+
## 文档
|
|
136
|
+
|
|
137
|
+
* [Markdown 文件内的元数据示例](https://github.com/cssmagic/kup/issues/1)
|
|
138
|
+
* [如何为 Kup 生成合适的 GitHub token](https://github.com/cssmagic/kup/issues/17)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## 其它
|
|
142
|
+
|
|
143
|
+
### 开发计划
|
|
144
|
+
|
|
145
|
+
* 请参考 [本项目的 issue 列表](https://github.com/cssmagic/kup/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 来了解当前规划。
|
|
146
|
+
* 欢迎提交需求,或参与 RFC 讨论。
|
|
147
|
+
|
|
148
|
+
### 关于名字
|
|
149
|
+
|
|
150
|
+
* Kup 取自 “皮卡” 的英文词 pickup,喻意是 “轻便地运载”。
|
|
151
|
+
* Kup 是变形金刚人物 “杯子” 的英文名。
|
|
152
|
+
|
|
153
|
+
### 关于 Logo
|
|
154
|
+
|
|
155
|
+
* Logo 由 [Fasil](https://freeicons.io/profile/722) 创作,并通过 [freeicons.io](https://freeicons.io/icon/e-commerce-icons/pickup-truck-icon-26893) 免费提供。
|
|
156
|
+
|
|
157
|
+
***
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
> Any code contributed to this project is considered authorized for commercial use by the project authors and their affiliated companies and distributed under this project's license.
|
|
162
|
+
>
|
|
163
|
+
> 任何贡献到本项目的代码,均视为授权本项目作者及其关联公司用于商业用途,并可按本项目协议进行分发。
|
|
164
|
+
|
|
165
|
+
MIT
|
package/bin/cli.js
CHANGED
|
@@ -14,8 +14,10 @@ if (isDebugging()) {
|
|
|
14
14
|
const argv = yargs(hideBin(process.argv))
|
|
15
15
|
.scriptName('kup')
|
|
16
16
|
.usage('Kup -- A CLI tool to sync local Markdown files to GitHub issues.')
|
|
17
|
-
.usage('Usage:
|
|
17
|
+
.usage('Usage (publish to GitHub): $0 <file> [options]')
|
|
18
|
+
.usage('Usage (dump to local file): $0 [file] --dump [options]')
|
|
18
19
|
.example('kup foo.md --repo aaa/bbb --id 123', '// sync foo.md to GitHub issue aaa/bbb#123')
|
|
20
|
+
.example('kup 123.md --dump --repo aaa/bbb --id 123', '// dump GitHub issue aaa/bbb#123 to 123.md')
|
|
19
21
|
.option('repo', {
|
|
20
22
|
alias: 'r',
|
|
21
23
|
type: 'string',
|
|
@@ -26,6 +28,11 @@ const argv = yargs(hideBin(process.argv))
|
|
|
26
28
|
type: 'number',
|
|
27
29
|
description: 'Specify GitHub issue ID',
|
|
28
30
|
})
|
|
31
|
+
.option('dump', {
|
|
32
|
+
alias: 'd',
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
description: 'Dump a GitHub issue to a local Markdown file',
|
|
35
|
+
})
|
|
29
36
|
.option('parse-only', {
|
|
30
37
|
alias: 'p',
|
|
31
38
|
type: 'boolean',
|
package/lib/file.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import fsPromises from 'fs/promises'
|
|
2
2
|
|
|
3
|
+
async function fileExists(pathname) {
|
|
4
|
+
try {
|
|
5
|
+
await fsPromises.access(pathname)
|
|
6
|
+
return true
|
|
7
|
+
} catch {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
function readTextFile(pathname) {
|
|
4
13
|
return fsPromises.readFile(pathname)
|
|
5
14
|
.then((bin) => {
|
|
@@ -16,6 +25,7 @@ function writeTextFile(pathname, content) {
|
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
export {
|
|
28
|
+
fileExists,
|
|
19
29
|
readTextFile,
|
|
20
30
|
writeTextFile,
|
|
21
31
|
}
|
package/lib/main.js
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
|
+
import path from 'path'
|
|
1
2
|
import inquirer from 'inquirer'
|
|
2
3
|
|
|
3
4
|
import { isDebugging } from './util.js'
|
|
4
5
|
import { KupError, errorLine } from './error.js'
|
|
5
|
-
import { readTextFile } from './file.js'
|
|
6
|
+
import { fileExists, readTextFile } from './file.js'
|
|
6
7
|
import { parse } from './parse.js'
|
|
7
8
|
import { getRepo } from './repo.js'
|
|
8
|
-
import { updateIssue, postIssue } from './sync.js'
|
|
9
|
+
import { dumpIssue, updateIssue, postIssue } from './sync.js'
|
|
9
10
|
import { getToken } from './token.js'
|
|
10
11
|
|
|
11
12
|
async function main(argv) {
|
|
12
13
|
const {
|
|
14
|
+
dump = false,
|
|
13
15
|
id = 0,
|
|
16
|
+
parseOnly = false,
|
|
14
17
|
repo = '',
|
|
15
18
|
_: files,
|
|
16
19
|
} = argv
|
|
17
20
|
|
|
18
21
|
// 目前只处理第一个文件
|
|
19
|
-
const file = files[0]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
errorLine(`[Kup] [Error] Cannot read file "${ file }"!`),
|
|
26
|
-
errorLine(e.message),
|
|
27
|
-
])
|
|
28
|
-
}
|
|
22
|
+
const file = files[0] || ''
|
|
23
|
+
const fileInfo = await getFileInfoForMode(file, {
|
|
24
|
+
dump,
|
|
25
|
+
parseOnly,
|
|
26
|
+
needsFileMeta: !repo || !id,
|
|
27
|
+
})
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (argv.parseOnly || isDebugging()) {
|
|
29
|
+
if (parseOnly || isDebugging()) {
|
|
33
30
|
console.log('[Kup] [Debug] fileInfo =', fileInfo)
|
|
34
31
|
// 如果有 p 参数,则提前退出,不需要走后面的步骤
|
|
35
|
-
if (
|
|
32
|
+
if (parseOnly) return
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
// 通过各种方式获取 repo
|
|
@@ -77,9 +74,19 @@ async function main(argv) {
|
|
|
77
74
|
|
|
78
75
|
// 检查 TOKEN
|
|
79
76
|
if (await getToken()) {
|
|
80
|
-
// 调用 GitHub API
|
|
81
77
|
const idReal = id || fileInfo.meta.id
|
|
82
|
-
if (
|
|
78
|
+
if (dump) {
|
|
79
|
+
if (!idReal) {
|
|
80
|
+
throw new KupError([
|
|
81
|
+
errorLine('[Kup] [Error] Cannot get `id` to dump!'),
|
|
82
|
+
])
|
|
83
|
+
}
|
|
84
|
+
const outputFile = file || path.resolve(`${ idReal }.md`)
|
|
85
|
+
await dumpIssue(repoReal, idReal, {
|
|
86
|
+
file: outputFile,
|
|
87
|
+
repoSource,
|
|
88
|
+
})
|
|
89
|
+
} else if (idReal) {
|
|
83
90
|
await updateIssue(fileInfo, repoReal, idReal)
|
|
84
91
|
} else {
|
|
85
92
|
await postIssue(fileInfo, repoReal, {
|
|
@@ -91,6 +98,39 @@ async function main(argv) {
|
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
async function getFileInfoForMode(file, {
|
|
102
|
+
dump = false,
|
|
103
|
+
parseOnly = false,
|
|
104
|
+
needsFileMeta = false,
|
|
105
|
+
} = {}) {
|
|
106
|
+
if (!dump || parseOnly) {
|
|
107
|
+
const content = await readMarkdownFile(file)
|
|
108
|
+
return parse(content)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!needsFileMeta || !file || !await fileExists(file)) {
|
|
112
|
+
return {
|
|
113
|
+
meta: {},
|
|
114
|
+
title: '',
|
|
115
|
+
content: '',
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const content = await readMarkdownFile(file)
|
|
120
|
+
return parse(content)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readMarkdownFile(file) {
|
|
124
|
+
try {
|
|
125
|
+
return await readTextFile(file)
|
|
126
|
+
} catch (e) {
|
|
127
|
+
throw new KupError([
|
|
128
|
+
errorLine(`[Kup] [Error] Cannot read file "${ file }"!`),
|
|
129
|
+
errorLine(e.message),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
94
134
|
function getRepoSourceLabel(source = '') {
|
|
95
135
|
switch (source) {
|
|
96
136
|
case 'package.repository':
|
package/lib/meta.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import YAML from 'yaml'
|
|
2
|
+
|
|
1
3
|
const SEPARATOR = '---'
|
|
2
4
|
const SEPARATOR_END_ALT = '...'
|
|
3
5
|
|
|
@@ -5,6 +7,8 @@ function updateIssueMeta(content, {
|
|
|
5
7
|
id,
|
|
6
8
|
repo = '',
|
|
7
9
|
shouldWriteRepo = false,
|
|
10
|
+
tags,
|
|
11
|
+
title,
|
|
8
12
|
} = {}) {
|
|
9
13
|
const eol = detectEol(content)
|
|
10
14
|
const lines = content.split(eol)
|
|
@@ -16,12 +20,16 @@ function updateIssueMeta(content, {
|
|
|
16
20
|
id,
|
|
17
21
|
repo,
|
|
18
22
|
shouldWriteRepo,
|
|
23
|
+
tags,
|
|
24
|
+
title,
|
|
19
25
|
})
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
const metaLines = lines.slice(metaSection.start + 1, metaSection.end)
|
|
23
29
|
upsertMetaLine(metaLines, 'repo', repo, shouldWriteRepo, ['id', 'tags', 'title'])
|
|
24
|
-
upsertMetaLine(metaLines, 'id',
|
|
30
|
+
upsertMetaLine(metaLines, 'id', id, typeof id !== 'undefined', ['tags', 'title'], ['repo'])
|
|
31
|
+
upsertMetaLine(metaLines, 'tags', tags, Array.isArray(tags) && tags.length > 0, ['title'], ['id', 'repo'])
|
|
32
|
+
upsertMetaLine(metaLines, 'title', title, typeof title === 'string' && !!title, [], ['tags', 'id', 'repo'])
|
|
25
33
|
|
|
26
34
|
return [
|
|
27
35
|
...lines.slice(0, metaSection.start + 1),
|
|
@@ -62,16 +70,30 @@ function detectMetaSection(lines) {
|
|
|
62
70
|
return null
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
function insertNewMetaSection(lines, {
|
|
73
|
+
function insertNewMetaSection(lines, {
|
|
74
|
+
eol,
|
|
75
|
+
id,
|
|
76
|
+
repo,
|
|
77
|
+
tags,
|
|
78
|
+
title,
|
|
79
|
+
shouldWriteRepo,
|
|
80
|
+
}) {
|
|
66
81
|
let insertAt = 0
|
|
67
82
|
while (insertAt < lines.length && !lines[insertAt].trim()) {
|
|
68
83
|
insertAt++
|
|
69
84
|
}
|
|
70
85
|
|
|
71
|
-
const metaLines = [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
const metaLines = [
|
|
87
|
+
SEPARATOR,
|
|
88
|
+
...buildMetaFields({
|
|
89
|
+
id,
|
|
90
|
+
repo,
|
|
91
|
+
tags,
|
|
92
|
+
title,
|
|
93
|
+
shouldWriteRepo,
|
|
94
|
+
}),
|
|
95
|
+
SEPARATOR,
|
|
96
|
+
]
|
|
75
97
|
|
|
76
98
|
const bodyLines = lines.slice(insertAt)
|
|
77
99
|
const result = [...metaLines]
|
|
@@ -93,7 +115,7 @@ function upsertMetaLine(metaLines, key, value, shouldWrite, beforeKeys = [], aft
|
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
const insertIndex = findInsertIndex(metaLines, beforeKeys, afterKeys)
|
|
96
|
-
metaLines.splice(insertIndex, 0,
|
|
118
|
+
metaLines.splice(insertIndex, 0, stringifyMetaField(key, value))
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
function findMetaLineIndex(metaLines, key) {
|
|
@@ -105,10 +127,10 @@ function findMetaLineIndex(metaLines, key) {
|
|
|
105
127
|
|
|
106
128
|
function replaceMetaLineValue(line, key, value) {
|
|
107
129
|
const match = new RegExp(`^(\\s*${ key }\\s*:)(\\s*)(.*?)(\\s+#.*)?$`).exec(line)
|
|
108
|
-
if (!match) return
|
|
130
|
+
if (!match) return stringifyMetaField(key, value)
|
|
109
131
|
|
|
110
132
|
const [, prefix, , , comment = ''] = match
|
|
111
|
-
return `${ prefix } ${ value }${ comment }`
|
|
133
|
+
return `${ prefix } ${ stringifyMetaValue(value, { flow: Array.isArray(value) }) }${ comment }`
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
function findInsertIndex(metaLines, beforeKeys, afterKeys) {
|
|
@@ -126,6 +148,61 @@ function findInsertIndex(metaLines, beforeKeys, afterKeys) {
|
|
|
126
148
|
return afterIndex >= 0 ? afterIndex + 1 : metaLines.length
|
|
127
149
|
}
|
|
128
150
|
|
|
151
|
+
function buildIssueMarkdown({
|
|
152
|
+
body = '',
|
|
153
|
+
id,
|
|
154
|
+
repo = '',
|
|
155
|
+
tags,
|
|
156
|
+
title,
|
|
157
|
+
shouldWriteRepo = false,
|
|
158
|
+
} = {}) {
|
|
159
|
+
const metaLines = [
|
|
160
|
+
SEPARATOR,
|
|
161
|
+
...buildMetaFields({
|
|
162
|
+
id,
|
|
163
|
+
repo,
|
|
164
|
+
tags,
|
|
165
|
+
title,
|
|
166
|
+
shouldWriteRepo,
|
|
167
|
+
}),
|
|
168
|
+
SEPARATOR,
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
return `${ metaLines.join('\n') }\n\n${ body || '' }`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildMetaFields({
|
|
175
|
+
id,
|
|
176
|
+
repo = '',
|
|
177
|
+
tags,
|
|
178
|
+
title,
|
|
179
|
+
shouldWriteRepo = false,
|
|
180
|
+
} = {}) {
|
|
181
|
+
const fields = []
|
|
182
|
+
if (shouldWriteRepo) fields.push(stringifyMetaField('repo', repo))
|
|
183
|
+
if (typeof id !== 'undefined') fields.push(stringifyMetaField('id', id))
|
|
184
|
+
if (Array.isArray(tags) && tags.length > 0) fields.push(stringifyMetaField('tags', tags))
|
|
185
|
+
if (typeof title === 'string' && title) fields.push(stringifyMetaField('title', title))
|
|
186
|
+
return fields
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stringifyMetaField(key, value) {
|
|
190
|
+
return `${ key }: ${ stringifyMetaValue(value, { flow: Array.isArray(value) }) }`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function stringifyMetaValue(value, { flow = false } = {}) {
|
|
194
|
+
const doc = new YAML.Document()
|
|
195
|
+
doc.contents = doc.createNode({ value })
|
|
196
|
+
const node = doc.get('value', true)
|
|
197
|
+
if (flow && node) node.flow = true
|
|
198
|
+
const output = doc.toString({
|
|
199
|
+
directives: false,
|
|
200
|
+
flowCollectionPadding: false,
|
|
201
|
+
}).trim()
|
|
202
|
+
return output.replace(/^value:\s*/, '')
|
|
203
|
+
}
|
|
204
|
+
|
|
129
205
|
export {
|
|
206
|
+
buildIssueMarkdown,
|
|
130
207
|
updateIssueMeta,
|
|
131
208
|
}
|
package/lib/parse.js
CHANGED
|
@@ -39,9 +39,11 @@ function parse(text) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function _detectMetaSectionLineQty(lines) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
42
|
+
const firstLine = lines[0] || ''
|
|
43
|
+
const secondLine = lines[1] || ''
|
|
44
|
+
const hasSeparatorStart = firstLine.trimEnd() === SEPARATOR
|
|
45
|
+
const noEmptyLineAfterSeparatorStart = !!secondLine.trimEnd()
|
|
46
|
+
const hasYamlKey = /^#?\s*[\w\-]+:/.test(secondLine.trim())
|
|
45
47
|
|
|
46
48
|
let metaLineQty = 0
|
|
47
49
|
if (hasSeparatorStart && noEmptyLineAfterSeparatorStart && hasYamlKey) {
|
|
@@ -111,7 +113,7 @@ function _stripMetaSection(lines, metaLineQty) {
|
|
|
111
113
|
if (emptyLineQty) lines.splice(0, emptyLineQty)
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
function _getTitleFromMainBody(firstLine) {
|
|
116
|
+
function _getTitleFromMainBody(firstLine = '') {
|
|
115
117
|
const re = /^#([^#].*)/
|
|
116
118
|
const result = re.exec(firstLine.trim())
|
|
117
119
|
const title = result ? result[1].trim() : ''
|
package/lib/sync.js
CHANGED
|
@@ -2,9 +2,9 @@ import ghGot from 'gh-got'
|
|
|
2
2
|
import inquirer from 'inquirer'
|
|
3
3
|
|
|
4
4
|
import { KupError, errorLine, logLine } from './error.js'
|
|
5
|
-
import { readTextFile, writeTextFile } from './file.js'
|
|
6
|
-
import { updateIssueMeta } from './meta.js'
|
|
7
|
-
import { isDebugging } from '
|
|
5
|
+
import { fileExists, readTextFile, writeTextFile } from './file.js'
|
|
6
|
+
import { buildIssueMarkdown, updateIssueMeta } from './meta.js'
|
|
7
|
+
import { isDebugging } from './util.js'
|
|
8
8
|
|
|
9
9
|
let proxyOptionsForGot = {}
|
|
10
10
|
if (isDebugging()) {
|
|
@@ -100,16 +100,101 @@ async function postIssue(fileInfo, repo, options = {}) {
|
|
|
100
100
|
console.log(`[Kup] [Success] URL: ${ url }`)
|
|
101
101
|
|
|
102
102
|
if (options.file) {
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
const shouldWriteMeta = await confirmWriteIssueMeta(options.file)
|
|
104
|
+
if (shouldWriteMeta) {
|
|
105
|
+
await writeIssueMeta(options.file, {
|
|
106
|
+
id,
|
|
107
|
+
repo,
|
|
108
|
+
shouldWriteRepo: options.repoSource === 'cli' && !options.hasRepoInMeta,
|
|
109
|
+
})
|
|
110
|
+
console.log(`[Kup] [Notice] Updated metadata in Markdown file: ${ options.file }`)
|
|
111
|
+
}
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
async function dumpIssue(repo, id, options = {}) {
|
|
117
|
+
const api = `/repos/${ repo }/issues/${ id }`
|
|
118
|
+
const url = `https://github.com/${ repo }/issues/${ id }`
|
|
119
|
+
let response = null
|
|
120
|
+
console.log(`[Kup] Dumping "${ repo }#${ id }"...`)
|
|
121
|
+
console.log(`[Kup] Dumping URL: ${ url }`)
|
|
122
|
+
try {
|
|
123
|
+
response = await ghGot(api, {
|
|
124
|
+
token: process.env.GITHUB_TOKEN,
|
|
125
|
+
...proxyOptionsForGot,
|
|
126
|
+
})
|
|
127
|
+
} catch (e) {
|
|
128
|
+
throw new KupError([
|
|
129
|
+
errorLine('[Kup] [Error] Request error: ' + e.message),
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!response) return
|
|
134
|
+
|
|
135
|
+
const outputFile = options.file
|
|
136
|
+
if (outputFile && await fileExists(outputFile)) {
|
|
137
|
+
const shouldOverwrite = await confirmOverwriteDumpFile(outputFile)
|
|
138
|
+
if (!shouldOverwrite) {
|
|
139
|
+
throw new KupError([
|
|
140
|
+
logLine('[Kup] Aborted!'),
|
|
141
|
+
])
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const issue = response.body || {}
|
|
146
|
+
const title = issue.title || ''
|
|
147
|
+
const body = issue.body || ''
|
|
148
|
+
const tags = Array.isArray(issue.labels)
|
|
149
|
+
? issue.labels
|
|
150
|
+
.map((label) => String(label?.name || '').trim())
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
: []
|
|
153
|
+
const content = buildIssueMarkdown({
|
|
154
|
+
body,
|
|
155
|
+
id,
|
|
156
|
+
repo,
|
|
157
|
+
shouldWriteRepo: options.repoSource === 'cli',
|
|
158
|
+
tags,
|
|
159
|
+
title,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await writeTextFile(outputFile, content)
|
|
164
|
+
} catch (e) {
|
|
165
|
+
throw new KupError([
|
|
166
|
+
errorLine(`[Kup] [Error] Cannot write file "${ outputFile }"!`),
|
|
167
|
+
errorLine(e.message),
|
|
168
|
+
])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`[Kup] [Success] Dumped "${ repo }#${ id }" to "${ outputFile }"!`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function confirmWriteIssueMeta(file) {
|
|
175
|
+
const answer = await inquirer.prompt([
|
|
176
|
+
{
|
|
177
|
+
name: 'writeIssueMeta',
|
|
178
|
+
type: 'confirm',
|
|
179
|
+
message: `Kup is going to write the new issue metadata back to "${ file }", OK?`,
|
|
180
|
+
default: true,
|
|
181
|
+
},
|
|
182
|
+
])
|
|
183
|
+
return answer.writeIssueMeta
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function confirmOverwriteDumpFile(file) {
|
|
187
|
+
const answer = await inquirer.prompt([
|
|
188
|
+
{
|
|
189
|
+
name: 'overwriteDumpFile',
|
|
190
|
+
type: 'confirm',
|
|
191
|
+
message: `Kup is going to overwrite the local file "${ file }", OK?`,
|
|
192
|
+
default: true,
|
|
193
|
+
},
|
|
194
|
+
])
|
|
195
|
+
return answer.overwriteDumpFile
|
|
196
|
+
}
|
|
197
|
+
|
|
113
198
|
async function writeIssueMeta(file, { id, repo, shouldWriteRepo }) {
|
|
114
199
|
let content = ''
|
|
115
200
|
try {
|
|
@@ -159,6 +244,9 @@ function buildPostIssuePayload(fileInfo) {
|
|
|
159
244
|
export {
|
|
160
245
|
buildPostIssuePayload,
|
|
161
246
|
buildUpdateIssuePayload,
|
|
247
|
+
confirmOverwriteDumpFile,
|
|
248
|
+
confirmWriteIssueMeta,
|
|
249
|
+
dumpIssue,
|
|
162
250
|
updateIssue,
|
|
163
251
|
postIssue,
|
|
164
252
|
writeIssueMeta,
|
package/lib/validate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
function validate(argv) {
|
|
2
2
|
const {
|
|
3
|
+
dump = false,
|
|
3
4
|
id,
|
|
4
5
|
repo,
|
|
5
6
|
_: files,
|
|
@@ -8,7 +9,8 @@ function validate(argv) {
|
|
|
8
9
|
let results = []
|
|
9
10
|
if ('id' in argv) results.push(validateId(id))
|
|
10
11
|
if ('repo' in argv) results.push(validateRepo(repo))
|
|
11
|
-
results.push(validateFiles(files))
|
|
12
|
+
results.push(validateFiles(files, { required: !dump }))
|
|
13
|
+
if (dump) results.push(validateDumpArgs(argv))
|
|
12
14
|
// 筛选出有错误的结果
|
|
13
15
|
const errorResults = results.filter((result) => result.status === false)
|
|
14
16
|
|
|
@@ -48,9 +50,9 @@ function validateRepo(repo) {
|
|
|
48
50
|
errorMsg,
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
function validateFiles(files) {
|
|
53
|
+
function validateFiles(files, { required = true } = {}) {
|
|
52
54
|
let errorMsg = ''
|
|
53
|
-
if (!files.length) {
|
|
55
|
+
if (required && !files.length) {
|
|
54
56
|
errorMsg = 'Must specify a local file!'
|
|
55
57
|
} else if (files.some((item) => !item)) {
|
|
56
58
|
errorMsg = 'File name must not be empty!'
|
|
@@ -61,8 +63,25 @@ function validateFiles(files) {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
function validateDumpArgs(argv) {
|
|
67
|
+
const {
|
|
68
|
+
_: files,
|
|
69
|
+
} = argv
|
|
70
|
+
|
|
71
|
+
let errorMsg = ''
|
|
72
|
+
if (!files.length && !('id' in argv)) {
|
|
73
|
+
errorMsg = 'Must specify a local file or `--id` in dump mode!'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
status: !errorMsg,
|
|
78
|
+
errorMsg,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
export {
|
|
65
83
|
validate,
|
|
84
|
+
validateDumpArgs,
|
|
66
85
|
validateId,
|
|
67
86
|
validateRepo,
|
|
68
87
|
validateFiles,
|