oneclickpush 0.1.0 → 0.3.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 -35
- package/cli.js +80 -37
- package/index.d.ts +40 -17
- package/index.js +63 -24
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -9,32 +9,35 @@ in one command.
|
|
|
9
9
|
[complicated](https://github.com/tschaub/gh-pages/tree/main/lib),
|
|
10
10
|
[buggy](https://github.com/tschaub/gh-pages/issues), and
|
|
11
11
|
[unmaintained](https://github.com/tschaub/gh-pages/issues/657#issue-4979248775); `oneclickpush` is
|
|
12
|
-
none of these, has zero dependencies and only uses Node
|
|
12
|
+
none of these, has zero dependencies and only uses built-in Node APIs + `git`.
|
|
13
13
|
|
|
14
14
|
## Quick Start
|
|
15
15
|
|
|
16
16
|
Deploy your build directory (e.g. `dist`) to the `gh-pages` branch:
|
|
17
17
|
|
|
18
18
|
```text
|
|
19
|
-
$
|
|
20
|
-
$ npx oneclickpush
|
|
19
|
+
$ npx oneclickpush dist
|
|
21
20
|
```
|
|
22
21
|
|
|
23
22
|
## CLI Usage
|
|
24
23
|
|
|
25
24
|
```text
|
|
26
|
-
Usage: oneclickpush [options]
|
|
25
|
+
Usage: oneclickpush <dir> [options]
|
|
27
26
|
|
|
28
27
|
Replace the contents of a remote git branch with that of a local directory.
|
|
29
28
|
|
|
29
|
+
Arguments:
|
|
30
|
+
dir Directory to deploy (required)
|
|
31
|
+
|
|
30
32
|
Options:
|
|
31
|
-
-
|
|
32
|
-
-e, --exclude <glob> Paths to leave out, relative to dist (default: "**/.DS_Store")
|
|
33
|
+
-e, --exclude <glob> Paths to leave out, relative to dir (default: "**/.DS_Store")
|
|
33
34
|
-b, --branch <name> Target branch (default: "gh-pages")
|
|
34
35
|
-m, --message <msg> Commit message (default: "Updates")
|
|
35
|
-
-
|
|
36
|
+
-r, --remote <name|url> Remote name or repo URL to deploy to (default: "origin")
|
|
37
|
+
-a, --author <author> Commit author, as "Name <email>"
|
|
38
|
+
-f, --force Force push new commit with no history
|
|
36
39
|
-n, --dry-run Show what would change, without committing or pushing
|
|
37
|
-
-
|
|
40
|
+
-l, --log-level <level> Output: silent, normal or verbose (default: "normal")
|
|
38
41
|
-V, --version Print version
|
|
39
42
|
-h, --help Show this help
|
|
40
43
|
```
|
|
@@ -43,20 +46,46 @@ Options:
|
|
|
43
46
|
> `--exclude` takes glob patterns (`*`, `**`, `?`) and can be repeated. Patterns are matched against
|
|
44
47
|
> the full path, so use `**/name` to match at any depth.
|
|
45
48
|
|
|
49
|
+
### Log levels
|
|
50
|
+
|
|
51
|
+
| Level | Behavior |
|
|
52
|
+
| --------- | -------------------------------------------------------------------- |
|
|
53
|
+
| `silent` | Prints nothing, not even errors. Check the exit code instead. |
|
|
54
|
+
| `normal` | Prints the result and error messages (plus a spinner in a terminal). |
|
|
55
|
+
| `verbose` | Also logs every git command to stderr and prints full errors. |
|
|
56
|
+
|
|
57
|
+
> [!WARNING]
|
|
58
|
+
> If your remote URL contains a token (e.g. `https://user:TOKEN@github.com/o/r.git`), git error
|
|
59
|
+
> messages shown at `normal` and `verbose` may include it. Use `-l silent` in CI, or keep tokens out
|
|
60
|
+
> of the URL with a credential helper.
|
|
61
|
+
|
|
46
62
|
### Examples
|
|
47
63
|
|
|
48
64
|
```bash
|
|
49
65
|
# Preview what would be deployed without pushing
|
|
50
|
-
npx oneclickpush
|
|
66
|
+
npx oneclickpush build --dry-run
|
|
51
67
|
|
|
52
68
|
# Custom branch and commit message
|
|
53
|
-
npx oneclickpush
|
|
69
|
+
npx oneclickpush public -b release -m "deploy: v1.0.0"
|
|
54
70
|
|
|
55
71
|
# Exclude source maps and test files
|
|
56
|
-
npx oneclickpush
|
|
72
|
+
npx oneclickpush dist -e "**/*.map" -e "**/tests/**"
|
|
73
|
+
|
|
74
|
+
# Deploy to a different remote or repository URL
|
|
75
|
+
npx oneclickpush dist -r upstream
|
|
76
|
+
npx oneclickpush dist -r git@github.com:user/repo.git
|
|
77
|
+
|
|
78
|
+
# Set commit author (useful in CI environments)
|
|
79
|
+
npx oneclickpush dist -a "Deploy Bot <bot@example.com>"
|
|
57
80
|
|
|
58
81
|
# Force push fresh history
|
|
59
|
-
npx oneclickpush
|
|
82
|
+
npx oneclickpush dist --force
|
|
83
|
+
|
|
84
|
+
# Silent output (useful in CI, check the exit code)
|
|
85
|
+
npx oneclickpush dist -l silent
|
|
86
|
+
|
|
87
|
+
# Log every git command
|
|
88
|
+
npx oneclickpush dist -l verbose
|
|
60
89
|
```
|
|
61
90
|
|
|
62
91
|
## Programmatic API
|
|
@@ -64,51 +93,68 @@ npx oneclickpush -d dist --force
|
|
|
64
93
|
You can also use `oneclickpush` as an ES module:
|
|
65
94
|
|
|
66
95
|
```bash
|
|
67
|
-
# Install
|
|
96
|
+
# Install
|
|
68
97
|
npm i oneclickpush
|
|
69
98
|
```
|
|
70
99
|
|
|
71
100
|
```javascript
|
|
72
|
-
import {
|
|
101
|
+
import { deploy } from 'oneclickpush'
|
|
73
102
|
|
|
74
|
-
const changes = await
|
|
75
|
-
|
|
103
|
+
const changes = await deploy({
|
|
104
|
+
dir: 'dist',
|
|
76
105
|
exclude: ['**/.DS_Store', '**/*.map'],
|
|
77
106
|
branch: 'gh-pages',
|
|
78
107
|
message: 'Deploy updates',
|
|
108
|
+
remote: 'origin',
|
|
109
|
+
author: 'Deploy Bot <bot@example.com>',
|
|
79
110
|
force: false,
|
|
80
111
|
dryRun: false,
|
|
81
|
-
|
|
112
|
+
logLevel: 'normal'
|
|
82
113
|
})
|
|
83
114
|
|
|
84
115
|
if (changes) {
|
|
85
|
-
console.log('
|
|
116
|
+
console.log('Deployed changes:\n' + changes)
|
|
86
117
|
} else {
|
|
87
|
-
console.log('Nothing to
|
|
118
|
+
console.log('Nothing to deploy')
|
|
88
119
|
}
|
|
89
120
|
```
|
|
90
121
|
|
|
122
|
+
The default values are also exported as `DEFAULTS`:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
import { DEFAULTS } from 'oneclickpush'
|
|
126
|
+
|
|
127
|
+
console.log(DEFAULTS.branch) // "gh-pages"
|
|
128
|
+
```
|
|
129
|
+
|
|
91
130
|
### Options
|
|
92
131
|
|
|
93
|
-
| Option
|
|
94
|
-
|
|
|
95
|
-
| `
|
|
96
|
-
| `exclude`
|
|
97
|
-
| `branch`
|
|
98
|
-
| `message`
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
132
|
+
| Option | Type | Default | Description |
|
|
133
|
+
| ---------- | ----------------------------------- | ------------------ | --------------------------------------------------- |
|
|
134
|
+
| `dir` | `string` | - (required) | Directory to deploy |
|
|
135
|
+
| `exclude` | `string[]` | `["**/.DS_Store"]` | Paths to exclude relative to `dir` |
|
|
136
|
+
| `branch` | `string` | `"gh-pages"` | Target git branch |
|
|
137
|
+
| `message` | `string` | `"Updates"` | Git commit message |
|
|
138
|
+
| `remote` | `string` | `"origin"` | Remote name or repository URL/path to deploy to |
|
|
139
|
+
| `author` | `string` | - | Commit author and committer, as `"Name <email>"` |
|
|
140
|
+
| `force` | `boolean` | `false` | Force push fresh commit with no previous history |
|
|
141
|
+
| `dryRun` | `boolean` | `false` | Return staged changes without committing or pushing |
|
|
142
|
+
| `logLevel` | `"silent" \| "normal" \| "verbose"` | `"normal"` | Only `"verbose"` has an effect: logs git commands |
|
|
143
|
+
|
|
144
|
+
> [!NOTE]
|
|
145
|
+
> The library never prints anything except at `"verbose"`. `"silent"` and `"normal"` only differ in
|
|
146
|
+
> the CLI.
|
|
102
147
|
|
|
103
148
|
## How It Works
|
|
104
149
|
|
|
105
|
-
1. Resolves the
|
|
150
|
+
1. Resolves the target repository URL from git remote configuration or uses the provided remote
|
|
151
|
+
URL/path directly.
|
|
106
152
|
2. Creates an isolated temporary directory and fetches the target branch (or initializes it if it
|
|
107
153
|
doesn't exist).
|
|
108
|
-
3. Clears the target branch index and stages the contents of `
|
|
154
|
+
3. Clears the target branch index and stages the contents of `dir` (minus `exclude` patterns)
|
|
109
155
|
directly via Git `work-tree`.
|
|
110
156
|
4. Checks for differences (`git diff --cached --name-status`). If there are changes, commits and
|
|
111
|
-
pushes back to
|
|
157
|
+
pushes back to the remote repository.
|
|
112
158
|
5. Cleans up the temporary directory.
|
|
113
159
|
|
|
114
160
|
> [!WARNING]
|
|
@@ -116,10 +162,14 @@ if (changes) {
|
|
|
116
162
|
|
|
117
163
|
### Notes
|
|
118
164
|
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
165
|
+
- The target branch is fully replaced with the contents of `dir` (WYSIWYG), and it's created if it
|
|
166
|
+
doesn't exist yet. If you need files like `.nojekyll` or `CNAME`, put them in `dir`, adding a
|
|
167
|
+
post-build step if your build tool wipes the directory.
|
|
168
|
+
- Dotfiles are included by default, except `.DS_Store`. Use `-e` to exclude more, but note that it
|
|
169
|
+
replaces the default list, so repeat `-e "**/.DS_Store"` if you still want that file excluded.
|
|
170
|
+
- When `--remote` is a repository URL or path, you don't need to run from inside a git repository.
|
|
171
|
+
- In dry-run output, identical content moved to a new path appears as a rename (`R100`).
|
|
172
|
+
- A directory starting with `-` must come after `--`, e.g. `oneclickpush -- -weird-dir`.
|
|
123
173
|
|
|
124
174
|
## License
|
|
125
175
|
|
package/cli.js
CHANGED
|
@@ -1,43 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util'
|
|
3
3
|
import { readFileSync } from 'node:fs'
|
|
4
|
-
import {
|
|
4
|
+
import { deploy, DEFAULTS } from './index.js'
|
|
5
5
|
|
|
6
|
-
const HELP = `Usage: oneclickpush [options]
|
|
6
|
+
const HELP = `Usage: oneclickpush <dir> [options]
|
|
7
7
|
|
|
8
|
-
Replace the contents of a remote git branch with that of a
|
|
8
|
+
Replace the contents of a remote git branch with that of a
|
|
9
9
|
local directory.
|
|
10
10
|
|
|
11
|
+
Arguments:
|
|
12
|
+
dir Directory to deploy (required)
|
|
13
|
+
|
|
11
14
|
Options:
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
-
|
|
15
|
+
-e, --exclude <glob> Paths to leave out, relative to dir
|
|
16
|
+
(default: "${DEFAULTS.exclude.join(', ')}")
|
|
17
|
+
-b, --branch <name> Target branch (default: "${DEFAULTS.branch}")
|
|
18
|
+
-m, --message <msg> Commit message (default: "${DEFAULTS.message}")
|
|
19
|
+
-r, --remote <name|url> Remote name or repo URL to deploy to
|
|
20
|
+
(default: "${DEFAULTS.remote}")
|
|
21
|
+
-a, --author <author> Commit author, as "Name <email>"
|
|
22
|
+
-f, --force Force push new commit with no history
|
|
23
|
+
-n, --dry-run Show what would change, without
|
|
19
24
|
committing or pushing
|
|
20
|
-
-
|
|
25
|
+
-l, --log-level <level> Output: silent, normal or verbose
|
|
26
|
+
(default: "${DEFAULTS.logLevel}")
|
|
21
27
|
-V, --version Print version
|
|
22
28
|
-h, --help Show this help
|
|
23
29
|
|
|
24
|
-
--exclude takes glob patterns (*, **, ?) and can be repeated.
|
|
25
|
-
Patterns are matched against the full path, so use **/name to
|
|
26
|
-
match at any depth
|
|
30
|
+
--exclude takes glob patterns (*, **, ?) and can be repeated.
|
|
31
|
+
Patterns are matched against the full path, so use **/name to
|
|
32
|
+
match at any depth.
|
|
33
|
+
|
|
34
|
+
Log levels: "silent" prints nothing (check the exit code),
|
|
35
|
+
"normal" prints the result and errors, "verbose" also logs git
|
|
36
|
+
commands and full errors.`
|
|
37
|
+
|
|
38
|
+
const LEVELS = ['silent', 'normal', 'verbose']
|
|
27
39
|
|
|
28
|
-
const { values: v } =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
const { values: v, positionals } = (() => {
|
|
41
|
+
try {
|
|
42
|
+
return parseArgs({
|
|
43
|
+
allowPositionals: true,
|
|
44
|
+
options: {
|
|
45
|
+
exclude: { type: 'string', short: 'e', multiple: true, default: DEFAULTS.exclude },
|
|
46
|
+
branch: { type: 'string', short: 'b', default: DEFAULTS.branch },
|
|
47
|
+
message: { type: 'string', short: 'm', default: DEFAULTS.message },
|
|
48
|
+
remote: { type: 'string', short: 'r', default: DEFAULTS.remote },
|
|
49
|
+
author: { type: 'string', short: 'a' },
|
|
50
|
+
force: { type: 'boolean', short: 'f', default: DEFAULTS.force },
|
|
51
|
+
'dry-run': { type: 'boolean', short: 'n', default: DEFAULTS.dryRun },
|
|
52
|
+
'log-level': { type: 'string', short: 'l', default: DEFAULTS.logLevel },
|
|
53
|
+
version: { type: 'boolean', short: 'V', default: false },
|
|
54
|
+
help: { type: 'boolean', short: 'h', default: false }
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
} catch (/** @type {any} */ e) {
|
|
58
|
+
if (!e.code?.startsWith('ERR_PARSE_ARGS_')) throw e
|
|
59
|
+
// Keep only the first sentence, Node appends a long "-- " hint to some messages
|
|
60
|
+
const msg = e.message.split('. ')[0].replace(/\.$/, '')
|
|
61
|
+
console.error(`${msg}, run "oneclickpush --help" for usage`)
|
|
62
|
+
process.exit(1)
|
|
39
63
|
}
|
|
40
|
-
})
|
|
64
|
+
})()
|
|
41
65
|
|
|
42
66
|
if (v.help) {
|
|
43
67
|
console.log(HELP)
|
|
@@ -50,27 +74,46 @@ if (v.version) {
|
|
|
50
74
|
process.exit(0)
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
|
|
77
|
+
if (positionals.length !== 1) {
|
|
78
|
+
console.error(
|
|
79
|
+
positionals.length === 0
|
|
80
|
+
? 'Missing required argument <dir>, run "oneclickpush --help" for usage'
|
|
81
|
+
: 'Expected exactly one directory, run "oneclickpush --help" for usage'
|
|
82
|
+
)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
const dir = positionals[0]
|
|
86
|
+
|
|
87
|
+
const level = /** @type {import('./index.js').OcpOptionsLogLevel} */ (v['log-level'])
|
|
88
|
+
if (!LEVELS.includes(level)) {
|
|
89
|
+
console.error(`Invalid --log-level "${level}", expected one of: ${LEVELS.join(', ')}`)
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Dead simple spinner (normal level, TTY only)
|
|
54
94
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
55
95
|
let i = 0
|
|
56
96
|
const spin =
|
|
57
|
-
process.stderr.isTTY &&
|
|
97
|
+
process.stderr.isTTY && level === 'normal'
|
|
58
98
|
? setInterval(() => process.stderr.write(`\r${frames[i++ % frames.length]}`), 100)
|
|
59
99
|
: null
|
|
60
100
|
const stop = () => spin && (clearInterval(spin), process.stderr.write('\r \r'))
|
|
61
101
|
|
|
62
102
|
try {
|
|
63
|
-
const changes = await
|
|
103
|
+
const changes = await deploy({ ...v, dir, logLevel: level, dryRun: v['dry-run'] })
|
|
64
104
|
stop()
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
if (level !== 'silent') {
|
|
106
|
+
console.log(
|
|
107
|
+
!changes
|
|
108
|
+
? 'Nothing to deploy'
|
|
109
|
+
: v['dry-run']
|
|
110
|
+
? `Dry run, would deploy:\n${changes}`
|
|
111
|
+
: 'Deployed!'
|
|
112
|
+
)
|
|
113
|
+
}
|
|
72
114
|
} catch (/** @type {any} */ e) {
|
|
73
115
|
stop()
|
|
74
|
-
console.error(
|
|
116
|
+
if (level === 'verbose') console.error(e)
|
|
117
|
+
else if (level === 'normal') console.error(e.message)
|
|
75
118
|
process.exit(1)
|
|
76
119
|
}
|
package/index.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
export type
|
|
1
|
+
export type OcpOptionsLogLevel = 'silent' | 'normal' | 'verbose'
|
|
2
|
+
/** @typedef {'silent' | 'normal' | 'verbose'} OcpOptionsLogLevel */
|
|
3
|
+
export declare const DEFAULTS: {
|
|
4
|
+
exclude: string[]
|
|
5
|
+
branch: string
|
|
6
|
+
message: string
|
|
7
|
+
remote: string
|
|
8
|
+
force: boolean
|
|
9
|
+
dryRun: boolean
|
|
10
|
+
logLevel: OcpOptionsLogLevel
|
|
11
|
+
}
|
|
12
|
+
export type OcpOptions = {
|
|
2
13
|
/**
|
|
3
|
-
* Directory to
|
|
14
|
+
* Directory to deploy (required)
|
|
4
15
|
*/
|
|
5
|
-
|
|
16
|
+
dir: string
|
|
6
17
|
/**
|
|
7
|
-
* Paths to leave out, relative to
|
|
18
|
+
* Paths to leave out, relative to dir (default: `**\/.DS_Store`)
|
|
8
19
|
*/
|
|
9
20
|
exclude?: string[]
|
|
10
21
|
/**
|
|
@@ -16,7 +27,15 @@ export type OneclickpushOptions = {
|
|
|
16
27
|
*/
|
|
17
28
|
message?: string
|
|
18
29
|
/**
|
|
19
|
-
*
|
|
30
|
+
* Remote name of the current repo, or a repository URL or path
|
|
31
|
+
*/
|
|
32
|
+
remote?: string
|
|
33
|
+
/**
|
|
34
|
+
* Commit author and committer as "Name <email>"
|
|
35
|
+
*/
|
|
36
|
+
author?: string
|
|
37
|
+
/**
|
|
38
|
+
* Force push new commit with no history
|
|
20
39
|
*/
|
|
21
40
|
force?: boolean
|
|
22
41
|
/**
|
|
@@ -24,32 +43,36 @@ export type OneclickpushOptions = {
|
|
|
24
43
|
*/
|
|
25
44
|
dryRun?: boolean
|
|
26
45
|
/**
|
|
27
|
-
*
|
|
46
|
+
* Only "verbose" logs (git commands, to stderr)
|
|
28
47
|
*/
|
|
29
|
-
|
|
48
|
+
logLevel?: OcpOptionsLogLevel
|
|
30
49
|
}
|
|
31
50
|
/**
|
|
32
|
-
* @typedef {object}
|
|
33
|
-
* @prop {string}
|
|
34
|
-
* @prop {string[]} [exclude] Paths to leave out, relative to
|
|
51
|
+
* @typedef {object} OcpOptions
|
|
52
|
+
* @prop {string} dir Directory to deploy (required)
|
|
53
|
+
* @prop {string[]} [exclude] Paths to leave out, relative to dir (default: `**\/.DS_Store`)
|
|
35
54
|
* @prop {string} [branch="gh-pages"] Target branch
|
|
36
55
|
* @prop {string} [message="Updates"] Commit message
|
|
37
|
-
* @prop {
|
|
56
|
+
* @prop {string} [remote="origin"] Remote name of the current repo, or a repository URL or path
|
|
57
|
+
* @prop {string} [author] Commit author and committer as "Name <email>"
|
|
58
|
+
* @prop {boolean} [force=false] Force push new commit with no history
|
|
38
59
|
* @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
|
|
39
|
-
* @prop {
|
|
60
|
+
* @prop {OcpOptionsLogLevel} [logLevel="normal"] Only "verbose" logs (git commands, to stderr)
|
|
40
61
|
*/
|
|
41
62
|
/**
|
|
42
63
|
* Replace the contents of a git branch with the contents of a directory.
|
|
43
64
|
* Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
|
|
44
|
-
* @param {
|
|
65
|
+
* @param {OcpOptions} options
|
|
45
66
|
* @returns {Promise<string>}
|
|
46
67
|
*/
|
|
47
|
-
export declare function
|
|
48
|
-
|
|
68
|
+
export declare function deploy({
|
|
69
|
+
dir,
|
|
49
70
|
exclude,
|
|
50
71
|
branch,
|
|
51
72
|
message,
|
|
73
|
+
remote,
|
|
74
|
+
author,
|
|
52
75
|
force,
|
|
53
76
|
dryRun,
|
|
54
|
-
|
|
55
|
-
}:
|
|
77
|
+
logLevel
|
|
78
|
+
}: OcpOptions): Promise<string>
|
package/index.js
CHANGED
|
@@ -4,42 +4,81 @@ import { mkdtemp, rm } from 'node:fs/promises'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join, resolve } from 'node:path'
|
|
6
6
|
|
|
7
|
+
/** @typedef {'silent' | 'normal' | 'verbose'} OcpOptionsLogLevel */
|
|
8
|
+
|
|
9
|
+
export const DEFAULTS = {
|
|
10
|
+
exclude: ['**/.DS_Store'],
|
|
11
|
+
branch: 'gh-pages',
|
|
12
|
+
message: 'Updates',
|
|
13
|
+
remote: 'origin',
|
|
14
|
+
force: false,
|
|
15
|
+
dryRun: false,
|
|
16
|
+
logLevel: /** @type {OcpOptionsLogLevel} */ ('normal')
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
/**
|
|
8
|
-
* @typedef {object}
|
|
9
|
-
* @prop {string}
|
|
10
|
-
* @prop {string[]} [exclude] Paths to leave out, relative to
|
|
20
|
+
* @typedef {object} OcpOptions
|
|
21
|
+
* @prop {string} dir Directory to deploy (required)
|
|
22
|
+
* @prop {string[]} [exclude] Paths to leave out, relative to dir (default: `**\/.DS_Store`)
|
|
11
23
|
* @prop {string} [branch="gh-pages"] Target branch
|
|
12
24
|
* @prop {string} [message="Updates"] Commit message
|
|
13
|
-
* @prop {
|
|
25
|
+
* @prop {string} [remote="origin"] Remote name of the current repo, or a repository URL or path
|
|
26
|
+
* @prop {string} [author] Commit author and committer as "Name <email>"
|
|
27
|
+
* @prop {boolean} [force=false] Force push new commit with no history
|
|
14
28
|
* @prop {boolean} [dryRun=false] Show what would change, without committing or pushing
|
|
15
|
-
* @prop {
|
|
29
|
+
* @prop {OcpOptionsLogLevel} [logLevel="normal"] Only "verbose" logs (git commands, to stderr)
|
|
16
30
|
*/
|
|
17
31
|
|
|
18
32
|
/**
|
|
19
33
|
* Replace the contents of a git branch with the contents of a directory.
|
|
20
34
|
* Returns the staged changes as `git diff --name-status` text or "" if nothing changed.
|
|
21
|
-
* @param {
|
|
35
|
+
* @param {OcpOptions} options
|
|
22
36
|
* @returns {Promise<string>}
|
|
23
37
|
*/
|
|
24
|
-
export async function
|
|
25
|
-
|
|
26
|
-
exclude =
|
|
27
|
-
branch =
|
|
28
|
-
message =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
export async function deploy({
|
|
39
|
+
dir,
|
|
40
|
+
exclude = DEFAULTS.exclude,
|
|
41
|
+
branch = DEFAULTS.branch,
|
|
42
|
+
message = DEFAULTS.message,
|
|
43
|
+
remote = DEFAULTS.remote,
|
|
44
|
+
author,
|
|
45
|
+
force = DEFAULTS.force,
|
|
46
|
+
dryRun = DEFAULTS.dryRun,
|
|
47
|
+
logLevel = DEFAULTS.logLevel
|
|
32
48
|
}) {
|
|
33
|
-
|
|
49
|
+
if (!dir) throw new Error('dir is required')
|
|
50
|
+
dir = resolve(dir)
|
|
34
51
|
const run = promisify(execFile)
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
// Env vars beat git config, so this also overrides any identity already set in CI
|
|
54
|
+
/** @type {Record<string, string>} */
|
|
55
|
+
let identity = {}
|
|
56
|
+
if (author) {
|
|
57
|
+
const m = /^(.+?)\s*<(.+)>$/.exec(author)
|
|
58
|
+
if (!m) throw new Error('--author must look like "Name <email>"')
|
|
59
|
+
identity = {
|
|
60
|
+
GIT_AUTHOR_NAME: m[1],
|
|
61
|
+
GIT_AUTHOR_EMAIL: m[2],
|
|
62
|
+
GIT_COMMITTER_NAME: m[1],
|
|
63
|
+
GIT_COMMITTER_EMAIL: m[2]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {(args: string[], cwd?: string, env?: Record<string, string>) => Promise<string>} */
|
|
68
|
+
const git = async (args, cwd, env) => {
|
|
69
|
+
if (logLevel === 'verbose') console.error(`$ git ${args.join(' ')}`)
|
|
70
|
+
const { stdout } = await run('git', args, { cwd, env: { ...process.env, ...env } })
|
|
71
|
+
return stdout.trim()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// A remote name resolves to its url; anything else is used as a url or path as-is
|
|
75
|
+
let url = remote
|
|
76
|
+
try {
|
|
77
|
+
url = await git(['remote', 'get-url', remote])
|
|
78
|
+
} catch {
|
|
79
|
+
if (/^[\w-]+$/.test(remote)) throw new Error(`Remote "${remote}" not found`)
|
|
40
80
|
}
|
|
41
81
|
|
|
42
|
-
const url = await git(['config', '--get', 'remote.origin.url'])
|
|
43
82
|
const tmp = await mkdtemp(join(tmpdir(), 'oneclickpush-'))
|
|
44
83
|
|
|
45
84
|
try {
|
|
@@ -57,27 +96,27 @@ export async function publish({
|
|
|
57
96
|
await git(['remote', 'add', 'origin', url], tmp)
|
|
58
97
|
}
|
|
59
98
|
|
|
60
|
-
// Empty the index, then stage everything from
|
|
99
|
+
// Empty the index, then stage everything from dir except excluded paths
|
|
61
100
|
await git(['read-tree', '--empty'], tmp)
|
|
62
101
|
await git(
|
|
63
102
|
[
|
|
64
103
|
'--git-dir',
|
|
65
104
|
join(tmp, '.git'),
|
|
66
105
|
'--work-tree',
|
|
67
|
-
|
|
106
|
+
dir,
|
|
68
107
|
'add',
|
|
69
108
|
'--force',
|
|
70
109
|
'--',
|
|
71
110
|
'.',
|
|
72
111
|
...exclude.map((p) => `:(exclude,glob)${p}`)
|
|
73
112
|
],
|
|
74
|
-
|
|
113
|
+
dir
|
|
75
114
|
)
|
|
76
115
|
|
|
77
116
|
const status = await git(['diff', '--cached', '--name-status'], tmp)
|
|
78
117
|
if (!status || dryRun) return status
|
|
79
118
|
|
|
80
|
-
await git(['commit', '-q', '-m', message], tmp)
|
|
119
|
+
await git(['commit', '-q', '-m', message], tmp, identity)
|
|
81
120
|
await git(['push', '-q', ...(force ? ['--force'] : []), 'origin', branch], tmp)
|
|
82
121
|
return status
|
|
83
122
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oneclickpush",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "One-click GitHub Pages deployer",
|
|
5
5
|
"author": "Jason Lee <jason@zerodevx.com>",
|
|
6
6
|
"scripts": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"keywords": [
|
|
34
34
|
"one-click",
|
|
35
35
|
"gh-pages",
|
|
36
|
-
"deploy"
|
|
36
|
+
"deploy",
|
|
37
|
+
"cli"
|
|
37
38
|
]
|
|
38
39
|
}
|