git-ftp 2.0.0.dev0__py3-none-any.whl
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.
- git_ftp-2.0.0.dev0.dist-info/METADATA +243 -0
- git_ftp-2.0.0.dev0.dist-info/RECORD +47 -0
- git_ftp-2.0.0.dev0.dist-info/WHEEL +4 -0
- git_ftp-2.0.0.dev0.dist-info/entry_points.txt +2 -0
- git_ftp-2.0.0.dev0.dist-info/licenses/LICENSE +674 -0
- gitftp/__init__.py +5 -0
- gitftp/__main__.py +7 -0
- gitftp/_version.py +24 -0
- gitftp/auth.py +209 -0
- gitftp/changeset.py +86 -0
- gitftp/cli/__init__.py +111 -0
- gitftp/cli/cmd_add_scope.py +23 -0
- gitftp/cli/cmd_catchup.py +19 -0
- gitftp/cli/cmd_download.py +19 -0
- gitftp/cli/cmd_help.py +30 -0
- gitftp/cli/cmd_init.py +19 -0
- gitftp/cli/cmd_log.py +31 -0
- gitftp/cli/cmd_pull.py +19 -0
- gitftp/cli/cmd_push.py +21 -0
- gitftp/cli/cmd_remove_scope.py +23 -0
- gitftp/cli/cmd_show.py +31 -0
- gitftp/cli/cmd_snapshot.py +20 -0
- gitftp/cli/cmd_unlock.py +19 -0
- gitftp/cli/cmd_version.py +19 -0
- gitftp/cli/group.py +68 -0
- gitftp/cli/options.py +208 -0
- gitftp/config.py +170 -0
- gitftp/deploy.py +341 -0
- gitftp/errors.py +86 -0
- gitftp/gitrepo.py +293 -0
- gitftp/hooks.py +30 -0
- gitftp/ignore.py +83 -0
- gitftp/include.py +94 -0
- gitftp/lock.py +94 -0
- gitftp/mirror.py +413 -0
- gitftp/options.py +72 -0
- gitftp/output.py +132 -0
- gitftp/session.py +212 -0
- gitftp/transfer.py +269 -0
- gitftp/transport/__init__.py +1 -0
- gitftp/transport/base.py +93 -0
- gitftp/transport/curlftp.py +389 -0
- gitftp/transport/listing.py +155 -0
- gitftp/transport/registry.py +73 -0
- gitftp/transport/sftp.py +442 -0
- gitftp/url.py +204 -0
- gitftp/version.py +35 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The ``snapshot`` action: turn a remote directory into a new repository."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from gitftp import mirror
|
|
10
|
+
from gitftp.cli.options import common_options, prepare, url_argument
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("snapshot", short_help="Download a remote directory into a new repository.")
|
|
14
|
+
@common_options
|
|
15
|
+
@url_argument
|
|
16
|
+
@click.argument("directory", required=False, metavar="[DIRECTORY]")
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def command(ctx: click.Context, /, url: str | None, directory: str | None, **kw: Any) -> None:
|
|
19
|
+
opts, out = prepare(ctx, kw)
|
|
20
|
+
mirror.run_snapshot(opts, url, directory, out)
|
gitftp/cli/cmd_unlock.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""The ``unlock`` action (new): remove a stale remote lock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from gitftp import mirror
|
|
10
|
+
from gitftp.cli.options import common_options, session_for, url_argument
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("unlock", short_help="Remove a stale remote lock left by an interrupted deploy.")
|
|
14
|
+
@common_options
|
|
15
|
+
@url_argument
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def command(ctx: click.Context, /, url: str | None, **kw: Any) -> None:
|
|
18
|
+
_opts, session = session_for(ctx, url, kw, need_repo=False)
|
|
19
|
+
mirror.run_unlock(session)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""The ``version`` action."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from gitftp.cli.options import common_options
|
|
10
|
+
from gitftp.version import runtime_info, version_line
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("version", short_help="Print the version.")
|
|
14
|
+
@common_options
|
|
15
|
+
def command(verbose: int = 0, **_kw: Any) -> None:
|
|
16
|
+
click.echo(version_line())
|
|
17
|
+
if verbose:
|
|
18
|
+
for line in runtime_info():
|
|
19
|
+
click.echo(line)
|
gitftp/cli/group.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""The click group and the action registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from gitftp.version import version_line
|
|
11
|
+
|
|
12
|
+
ACTIONS: tuple[str, ...] = (
|
|
13
|
+
"init",
|
|
14
|
+
"push",
|
|
15
|
+
"catchup",
|
|
16
|
+
"show",
|
|
17
|
+
"log",
|
|
18
|
+
"download",
|
|
19
|
+
"pull",
|
|
20
|
+
"snapshot",
|
|
21
|
+
"add-scope",
|
|
22
|
+
"remove-scope",
|
|
23
|
+
"unlock",
|
|
24
|
+
"help",
|
|
25
|
+
"version",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_MODULES = {name: f"gitftp.cli.cmd_{name.replace('-', '_')}" for name in ACTIONS}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GitFtpGroup(click.Group):
|
|
32
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
33
|
+
return list(ACTIONS)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _print_version(ctx: click.Context, _param: click.Parameter, value: bool) -> None:
|
|
37
|
+
if not value or ctx.resilient_parsing:
|
|
38
|
+
return
|
|
39
|
+
click.echo(version_line())
|
|
40
|
+
ctx.exit(0)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_group() -> click.Group:
|
|
44
|
+
@click.group(
|
|
45
|
+
cls=GitFtpGroup,
|
|
46
|
+
name="git-ftp",
|
|
47
|
+
context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 100},
|
|
48
|
+
invoke_without_command=True,
|
|
49
|
+
help="Git powered FTP, FTPS, FTPES and SFTP client.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--version",
|
|
53
|
+
is_flag=True,
|
|
54
|
+
expose_value=False,
|
|
55
|
+
is_eager=True,
|
|
56
|
+
callback=_print_version,
|
|
57
|
+
help="Print the version and exit.",
|
|
58
|
+
)
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def group(ctx: click.Context, /, **_kw: Any) -> None:
|
|
61
|
+
if ctx.invoked_subcommand is None:
|
|
62
|
+
click.echo("git-ftp <action> [<options>] [<url>]")
|
|
63
|
+
ctx.exit(2)
|
|
64
|
+
|
|
65
|
+
for name, module_name in _MODULES.items():
|
|
66
|
+
module = importlib.import_module(module_name)
|
|
67
|
+
group.add_command(module.command, name=name)
|
|
68
|
+
return group
|
gitftp/cli/options.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Options shared by every action, plus helpers for command modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from gitftp.options import CliOptions
|
|
12
|
+
from gitftp.output import Output
|
|
13
|
+
from gitftp.session import Session, open_session
|
|
14
|
+
|
|
15
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
16
|
+
|
|
17
|
+
# Options that take a value; the CLI normaliser must not mistake the value for the action.
|
|
18
|
+
VALUE_OPTIONS = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"-u",
|
|
21
|
+
"--user",
|
|
22
|
+
"-p",
|
|
23
|
+
"--passwd",
|
|
24
|
+
"--password",
|
|
25
|
+
"--password-command",
|
|
26
|
+
"-k",
|
|
27
|
+
"--keychain",
|
|
28
|
+
"--key",
|
|
29
|
+
"--pubkey",
|
|
30
|
+
"--key-passphrase",
|
|
31
|
+
"-b",
|
|
32
|
+
"--branch",
|
|
33
|
+
"-c",
|
|
34
|
+
"--commit",
|
|
35
|
+
"-s",
|
|
36
|
+
"--scope",
|
|
37
|
+
"--syncroot",
|
|
38
|
+
"--remote-root",
|
|
39
|
+
"--cacert",
|
|
40
|
+
"-x",
|
|
41
|
+
"--proxy",
|
|
42
|
+
"-j",
|
|
43
|
+
"--jobs",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
OPTIONAL_VALUE_OPTIONS = frozenset({"-u", "--user", "-k", "--keychain", "-s", "--scope"})
|
|
47
|
+
_LONG = {"-u": "--user", "-k": "--keychain", "-s": "--scope"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def long_name(opt: str) -> str:
|
|
51
|
+
return _LONG.get(opt, opt)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def common_options(f: F) -> F:
|
|
55
|
+
decorators = [
|
|
56
|
+
click.option(
|
|
57
|
+
"-u",
|
|
58
|
+
"--user",
|
|
59
|
+
"user",
|
|
60
|
+
is_flag=False,
|
|
61
|
+
flag_value="",
|
|
62
|
+
default=None,
|
|
63
|
+
metavar="[USER]",
|
|
64
|
+
help="FTP login name (bare -u: the local user).",
|
|
65
|
+
),
|
|
66
|
+
click.option(
|
|
67
|
+
"-p",
|
|
68
|
+
"--passwd",
|
|
69
|
+
"--password",
|
|
70
|
+
"password",
|
|
71
|
+
default=None,
|
|
72
|
+
metavar="PASSWORD",
|
|
73
|
+
help="FTP password.",
|
|
74
|
+
),
|
|
75
|
+
click.option(
|
|
76
|
+
"-P",
|
|
77
|
+
"--ask-passwd",
|
|
78
|
+
"ask_password",
|
|
79
|
+
is_flag=True,
|
|
80
|
+
help="Ask for the password interactively.",
|
|
81
|
+
),
|
|
82
|
+
click.option(
|
|
83
|
+
"--password-command",
|
|
84
|
+
default=None,
|
|
85
|
+
metavar="CMD",
|
|
86
|
+
help="Shell command whose first output line is the password.",
|
|
87
|
+
),
|
|
88
|
+
click.option(
|
|
89
|
+
"-k",
|
|
90
|
+
"--keychain",
|
|
91
|
+
"keychain",
|
|
92
|
+
is_flag=False,
|
|
93
|
+
flag_value="",
|
|
94
|
+
default=None,
|
|
95
|
+
metavar="[[ACCOUNT]@[HOST]]",
|
|
96
|
+
help="macOS keychain entry (bare -k: guess).",
|
|
97
|
+
),
|
|
98
|
+
click.option("--key", default=None, metavar="FILE", help="SFTP private key."),
|
|
99
|
+
click.option("--pubkey", default=None, metavar="FILE", help="SFTP public key."),
|
|
100
|
+
click.option(
|
|
101
|
+
"--key-passphrase",
|
|
102
|
+
default=None,
|
|
103
|
+
metavar="TEXT",
|
|
104
|
+
help="Passphrase of the SFTP private key.",
|
|
105
|
+
),
|
|
106
|
+
click.option(
|
|
107
|
+
"-b",
|
|
108
|
+
"--branch",
|
|
109
|
+
default=None,
|
|
110
|
+
metavar="BRANCH",
|
|
111
|
+
help="Deploy this branch instead of the current one.",
|
|
112
|
+
),
|
|
113
|
+
click.option(
|
|
114
|
+
"-c",
|
|
115
|
+
"--commit",
|
|
116
|
+
default=None,
|
|
117
|
+
metavar="SHA",
|
|
118
|
+
help="Treat SHA as the deployed commit instead of reading the remote log.",
|
|
119
|
+
),
|
|
120
|
+
click.option(
|
|
121
|
+
"-s",
|
|
122
|
+
"--scope",
|
|
123
|
+
"scope",
|
|
124
|
+
is_flag=False,
|
|
125
|
+
flag_value="",
|
|
126
|
+
default=None,
|
|
127
|
+
metavar="[SCOPE]",
|
|
128
|
+
help="Configuration scope (bare -s: current branch).",
|
|
129
|
+
),
|
|
130
|
+
click.option(
|
|
131
|
+
"--syncroot",
|
|
132
|
+
default=None,
|
|
133
|
+
metavar="DIR",
|
|
134
|
+
help="Deploy only this directory, as the remote root.",
|
|
135
|
+
),
|
|
136
|
+
click.option(
|
|
137
|
+
"--remote-root",
|
|
138
|
+
default=None,
|
|
139
|
+
metavar="DIR",
|
|
140
|
+
help="Remote directory, replacing the path in the URL.",
|
|
141
|
+
),
|
|
142
|
+
click.option(
|
|
143
|
+
"--cacert", default=None, metavar="FILE", help="CA certificate bundle for FTPS/FTPES."
|
|
144
|
+
),
|
|
145
|
+
click.option("-x", "--proxy", default=None, metavar="URL", help="Proxy URL."),
|
|
146
|
+
click.option(
|
|
147
|
+
"-j",
|
|
148
|
+
"--jobs",
|
|
149
|
+
type=int,
|
|
150
|
+
default=None,
|
|
151
|
+
metavar="N",
|
|
152
|
+
help="Parallel connections (default 4, 1 = sequential).",
|
|
153
|
+
),
|
|
154
|
+
click.option(
|
|
155
|
+
"-a", "--all", "all", is_flag=True, help="Upload all files, not only changes."
|
|
156
|
+
),
|
|
157
|
+
click.option("-A", "--active", is_flag=True, help="Use FTP active mode."),
|
|
158
|
+
click.option("-l", "--lock", is_flag=True, help="Lock the remote during the deploy."),
|
|
159
|
+
click.option("-D", "--dry-run", is_flag=True, help="Show what would happen."),
|
|
160
|
+
click.option("-f", "--force", is_flag=True, help="Skip the lock check and questions."),
|
|
161
|
+
click.option("-n", "--silent", is_flag=True, help="Print nothing but fatal errors."),
|
|
162
|
+
click.option("-v", "--verbose", count=True, help="Verbose (-vv: protocol trace)."),
|
|
163
|
+
click.option(
|
|
164
|
+
"--insecure", is_flag=True, help="Do not verify TLS certificates / host keys."
|
|
165
|
+
),
|
|
166
|
+
click.option("--disable-epsv", is_flag=True, help="Use PASV instead of EPSV."),
|
|
167
|
+
click.option("--no-commit", is_flag=True, help="pull: merge without committing."),
|
|
168
|
+
click.option(
|
|
169
|
+
"--changed-only",
|
|
170
|
+
is_flag=True,
|
|
171
|
+
help="download/pull: only files that changed locally as well.",
|
|
172
|
+
),
|
|
173
|
+
click.option("--no-verify", is_flag=True, help="Skip the pre-ftp-push hook."),
|
|
174
|
+
click.option("--no-post-hooks", is_flag=True, help="Skip the post-ftp-push hook."),
|
|
175
|
+
click.option(
|
|
176
|
+
"--enable-post-errors", is_flag=True, help="Fail when the post-ftp-push hook fails."
|
|
177
|
+
),
|
|
178
|
+
click.option("--auto-init", is_flag=True, help="push: init when the remote has no log."),
|
|
179
|
+
click.option(
|
|
180
|
+
"--worktree",
|
|
181
|
+
is_flag=True,
|
|
182
|
+
help="Upload files from a temporary git worktree so edits to the working "
|
|
183
|
+
"tree during the upload are ignored.",
|
|
184
|
+
),
|
|
185
|
+
]
|
|
186
|
+
for d in reversed(decorators):
|
|
187
|
+
f = d(f)
|
|
188
|
+
return f
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
url_argument = click.argument("url", required=False, metavar="[URL]")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def prepare(ctx: click.Context, kw: dict[str, Any]) -> tuple[CliOptions, Output]:
|
|
195
|
+
opts = CliOptions.from_kwargs(kw)
|
|
196
|
+
out: Output = ctx.obj if isinstance(ctx.obj, Output) else Output()
|
|
197
|
+
out.level = opts.level
|
|
198
|
+
ctx.obj = out
|
|
199
|
+
return opts, out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def session_for(
|
|
203
|
+
ctx: click.Context, url: str | None, kw: dict[str, Any], *, need_repo: bool = True
|
|
204
|
+
) -> tuple[CliOptions, Session]:
|
|
205
|
+
opts, out = prepare(ctx, kw)
|
|
206
|
+
session = open_session(opts, url, out, cwd=Path.cwd(), need_repo=need_repo)
|
|
207
|
+
ctx.call_on_close(session.close)
|
|
208
|
+
return opts, session
|
gitftp/config.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Configuration lookup with upstream's precedence.
|
|
2
|
+
|
|
3
|
+
Order for a key ``k`` with scope ``s`` (first *found* wins, even if the value
|
|
4
|
+
is empty, because ``git-ftp.<scope>.url ""`` is the documented way to mask a
|
|
5
|
+
default):
|
|
6
|
+
|
|
7
|
+
1. ``.git-ftp-config`` in the repository: ``git-ftp.<s>.k``
|
|
8
|
+
2. ``.git-ftp-config``: ``git-ftp.k``
|
|
9
|
+
3. git config (system, global, local merged): ``git-ftp.<s>.k``
|
|
10
|
+
4. git config: ``git-ftp.k``
|
|
11
|
+
|
|
12
|
+
A valueless key (a bare ``insecure`` line) is read as true, as git reads it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from gitftp import url as urlmod
|
|
22
|
+
from gitftp.errors import GitError, MissingArgumentError, UsageError
|
|
23
|
+
from gitftp.gitrepo import GitRunner
|
|
24
|
+
|
|
25
|
+
SECTION = "git-ftp"
|
|
26
|
+
CONFIG_FILE = ".git-ftp-config"
|
|
27
|
+
DEFAULT_DEPLOYED_SHA1_FILE = ".git-ftp.log"
|
|
28
|
+
DEFAULT_JOBS = 4
|
|
29
|
+
SCOPE_RE = re.compile(r"^[-0-9a-zA-Z_/]+$")
|
|
30
|
+
|
|
31
|
+
KEYS = (
|
|
32
|
+
"url",
|
|
33
|
+
"user",
|
|
34
|
+
"password",
|
|
35
|
+
"password-command",
|
|
36
|
+
"keychain",
|
|
37
|
+
"cacert",
|
|
38
|
+
"insecure",
|
|
39
|
+
"disable-epsv",
|
|
40
|
+
"proxy",
|
|
41
|
+
"no-commit",
|
|
42
|
+
"branch",
|
|
43
|
+
"syncroot",
|
|
44
|
+
"key",
|
|
45
|
+
"pubkey",
|
|
46
|
+
"key-passphrase",
|
|
47
|
+
"remote-root",
|
|
48
|
+
"deployedsha1file",
|
|
49
|
+
"jobs",
|
|
50
|
+
"worktree",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_TRUE = frozenset({"true", "yes", "on", "1"})
|
|
54
|
+
_FALSE = frozenset({"false", "no", "off", "0", ""})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_bool(value: str | None) -> bool | None:
|
|
58
|
+
"""Git's boolean spellings. ``None`` (valueless) is true; unknown text is ``None``."""
|
|
59
|
+
if value is None:
|
|
60
|
+
return True
|
|
61
|
+
v = value.strip().lower()
|
|
62
|
+
if v in _TRUE:
|
|
63
|
+
return True
|
|
64
|
+
if v in _FALSE:
|
|
65
|
+
return False
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Config:
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
scope: str | None,
|
|
73
|
+
file_cfg: Mapping[str, str | None],
|
|
74
|
+
git_cfg: Mapping[str, str | None],
|
|
75
|
+
) -> None:
|
|
76
|
+
self.scope = scope
|
|
77
|
+
self._sources: tuple[Mapping[str, str | None], ...] = (file_cfg, git_cfg)
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def load(cls, git: GitRunner, root: Path | None, scope: str | None) -> Config:
|
|
81
|
+
file_cfg: Mapping[str, str | None] = {}
|
|
82
|
+
base = root if root is not None else git.cwd
|
|
83
|
+
cfg_file = base / CONFIG_FILE
|
|
84
|
+
if cfg_file.is_file():
|
|
85
|
+
file_cfg = git.config_list(cfg_file)
|
|
86
|
+
git_cfg = git.config_list()
|
|
87
|
+
return cls(scope, file_cfg, git_cfg)
|
|
88
|
+
|
|
89
|
+
# -- lookups -----------------------------------------------------------
|
|
90
|
+
def lookup_raw(self, key: str) -> tuple[bool, str | None]:
|
|
91
|
+
"""(found, value); value is None for a valueless key."""
|
|
92
|
+
for src in self._sources:
|
|
93
|
+
if self.scope:
|
|
94
|
+
scoped = f"{SECTION}.{self.scope}.{key}"
|
|
95
|
+
if scoped in src:
|
|
96
|
+
return True, src[scoped]
|
|
97
|
+
plain = f"{SECTION}.{key}"
|
|
98
|
+
if plain in src:
|
|
99
|
+
return True, src[plain]
|
|
100
|
+
return False, None
|
|
101
|
+
|
|
102
|
+
def lookup(self, key: str) -> str | None:
|
|
103
|
+
"""The value, or ``None`` when the key is absent. A valueless key reads as ''."""
|
|
104
|
+
found, value = self.lookup_raw(key)
|
|
105
|
+
if not found:
|
|
106
|
+
return None
|
|
107
|
+
return "" if value is None else value
|
|
108
|
+
|
|
109
|
+
def get(self, key: str, default: str = "") -> str:
|
|
110
|
+
value = self.lookup(key)
|
|
111
|
+
return default if value is None else value
|
|
112
|
+
|
|
113
|
+
def get_bool(self, key: str, default: bool = False) -> bool:
|
|
114
|
+
found, value = self.lookup_raw(key)
|
|
115
|
+
if not found:
|
|
116
|
+
return default
|
|
117
|
+
parsed = parse_bool(value)
|
|
118
|
+
if parsed is None:
|
|
119
|
+
raise UsageError(f"Invalid boolean value '{value}' for git-ftp.{key}.")
|
|
120
|
+
return parsed
|
|
121
|
+
|
|
122
|
+
def get_int(self, key: str, default: int) -> int:
|
|
123
|
+
value = self.lookup(key)
|
|
124
|
+
if value is None or value == "":
|
|
125
|
+
return default
|
|
126
|
+
try:
|
|
127
|
+
return int(value)
|
|
128
|
+
except ValueError:
|
|
129
|
+
raise UsageError(f"Invalid number '{value}' for git-ftp.{key}.") from None
|
|
130
|
+
|
|
131
|
+
def git_option(self, dotted: str) -> str | None:
|
|
132
|
+
"""An ordinary (never scoped) git option such as ``http.proxy``."""
|
|
133
|
+
value = self._sources[1].get(dotted.lower())
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# -- scopes -----------------------------------------------------------------
|
|
138
|
+
def validate_scope(name: str, *, from_option: bool) -> str:
|
|
139
|
+
if not name:
|
|
140
|
+
raise MissingArgumentError("Missing scope argument.")
|
|
141
|
+
if not SCOPE_RE.match(name):
|
|
142
|
+
if from_option:
|
|
143
|
+
raise UsageError(f"Invalid scope name '{name}'.")
|
|
144
|
+
raise UsageError("Invalid scope name. Only these characters are allowed: 0-9 a-z A-Z - _ /")
|
|
145
|
+
return name
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def add_scope(git: GitRunner, scope: str, raw_url: str) -> None:
|
|
149
|
+
"""``git ftp add-scope``: store url/user/password in the local git config."""
|
|
150
|
+
validate_scope(scope, from_option=False)
|
|
151
|
+
if not raw_url:
|
|
152
|
+
raise MissingArgumentError("Missing URL argument.")
|
|
153
|
+
u = urlmod.parse(raw_url)
|
|
154
|
+
lead = "/" if u.absolute else ""
|
|
155
|
+
bare = (
|
|
156
|
+
f"{u.scheme.value}://{u.host}/{lead}{u.path}".rstrip("/")
|
|
157
|
+
if u.path
|
|
158
|
+
else (f"{u.scheme.value}://{u.host}/{lead}".rstrip("/"))
|
|
159
|
+
)
|
|
160
|
+
git.config_set(f"{SECTION}.{scope}.url", bare)
|
|
161
|
+
if u.user is not None:
|
|
162
|
+
git.config_set(f"{SECTION}.{scope}.user", u.user)
|
|
163
|
+
if u.password is not None:
|
|
164
|
+
git.config_set(f"{SECTION}.{scope}.password", u.password)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def remove_scope(git: GitRunner, scope: str) -> None:
|
|
168
|
+
validate_scope(scope, from_option=False)
|
|
169
|
+
if not git.config_remove_section(f"{SECTION}.{scope}"):
|
|
170
|
+
raise GitError(f"Cannot find scope {scope}.")
|