This release is a pre-release and may not be stable for production use.
git-ftp (Python)
This is a native Python port of the Bash git-ftp: deploy a Git repository to a server over FTP, FTPS, FTPES or SFTP, uploading only the files that changed since the last deployment.
git-ftp records the deployed commit in a file on the remote (.git-ftp.log).
On the next push it diffs that commit against HEAD and transfers exactly the
files that were added, modified or deleted, in parallel. No server-side
software is needed.
If your server forbids writing dot-files (some hardened FTP servers reject any
name starting with a .), set a non-dot-file name with
git config git-ftp.deployedsha1file gitftp.log.
This port reads the same configuration and the same remote files as the Bash
original, so an existing deployment carries over unchanged. It needs Python 3.10
or newer and git; libcurl comes bundled with the pycurl wheel and SFTP is
spoken by paramiko. See COMPATIBILITY.md for what was
kept, fixed and added.
Install
pip install git-ftp # or: pipx install git-ftp, uv tool install git-ftp
git ftp --version
Git runs the git-ftp script as git ftp when it is on your PATH.
Usage
# First deployment: upload everything and record the commit.
git ftp init -u alice -P ftp://example.com/public_html
# Every deployment after that: only what changed.
git ftp push -u alice -P ftp://example.com/public_html
# The remote already has the current files? Just record the commit.
git ftp catchup ftp://example.com/public_html
# What is deployed?
git ftp show
git ftp log
-P prompts for the password. Better than typing it every time:
git config git-ftp.url ftp://example.com/public_html
git config git-ftp.user alice
git config git-ftp.password s3cret # or:
git config git-ftp.password-command "pass show example.com/ftp"
git ftp push
In CI, GIT_FTP_URL, GIT_FTP_USER and GIT_FTP_PASSWORD do the same without
touching any file.
Scopes
Several targets in one repository:
git ftp add-scope production ftp://alice:s3cret@live.example.com/htdocs
git ftp add-scope staging ftp://alice:s3cret@staging.example.com/htdocs
git ftp push -s production
git ftp push -s # bare -s: the current branch name is the scope
Scope keys (git-ftp.<scope>.<key>) override the plain keys; an explicit empty
scope value masks the default.
Protocols
| URL | Transport | Encryption |
|---|---|---|
ftp://host/path |
libcurl | none |
ftpes://host/path |
libcurl | explicit TLS (AUTH TLS), data channel too |
ftps://host/path |
libcurl | implicit TLS (port 990) |
sftp://host/path |
paramiko | SSH |
TLS certificates are verified; use --cacert FILE for a private CA or
--insecure to skip verification. SFTP host keys are checked against
~/.ssh/known_hosts (ssh-keyscan host >> ~/.ssh/known_hosts to add one).
SFTP authenticates with --key FILE (--key-passphrase for encrypted keys),
a running ssh-agent, or a password. sftp://host/~/dir and
sftp://host//absolute/dir work as in curl.
Choosing what to deploy
--syncroot DIRdeploys onlyDIR, withDIRas the remote root..git-ftp-ignorelists shell globs of Git paths never to upload (*also matches/, and a pattern must match the whole path)..git-ftp-includeuploads untracked files:!VERSION.txtalways, orcss/style.css:scss/style.scsswhenever the tracked source changed. A directory target (vendor/:composer.lock) uploads everything below it.--dry-runshows the plan;-auploads everything;-c SHAdiffs against a specific commit;-b BRANCHdeploys another branch.
Parallel transfers
Files are transferred over up to four connections. --jobs N or
git config git-ftp.jobs N changes that; --jobs 1 is sequential. Uploads
happen first, then deletes, and the commit log is written last, only when every
upload succeeded, so an interrupted deploy never claims a commit it did not
finish. Ctrl-C stops promptly.
In an interactive terminal a spinner shows a done/total count with the current
file on stderr. It is off when output is piped, in CI, or under -n, so scripts
see the plain lines unchanged.
Consistent uploads while editing
--worktree, or git config git-ftp.worktree true, reads the files to upload
from a throwaway Git worktree checked out at the commit being deployed. Editing
the working tree while a long upload runs then cannot change what is sent. The
worktree is removed when the deploy finishes.
Hooks and locking
.git/hooks/pre-ftp-push (veto with a non-zero exit; skipped by --no-verify)
and post-ftp-push (--enable-post-errors makes its failure fatal) receive
<scope-or-host> <url> <local-commit> <deployed-commit>; the pre hook also gets
the NUL-separated A path / D path change list on stdin.
--lock writes git-ftp.lck on the remote for the duration of the deploy;
another deploy of a different commit is refused with exit 7. git ftp unlock
removes a stale lock.
download, pull, snapshot
These mirror the remote into the working tree:
git ftp download # remote -> working tree (refuses to run with untracked files)
git ftp pull # download into a commit on the deployed revision, then merge
git ftp snapshot ftp://example.com/htdocs [dir] # new repository from a remote
--changed-only limits pull to files that changed locally as well;
--no-commit (or git-ftp.no-commit) leaves the merge uncommitted.
Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | unexpected error |
| 2 | wrong usage |
| 3 | missing argument |
| 4 | error while uploading (also: remote unreachable, login failed) |
| 5 | error while downloading (also: push before init) |
| 6 | unknown protocol |
| 7 | remote locked |
| 8 | git error (not a repository, dirty working tree, bad branch) |
| 9 | hook failed |
| 10 | local filesystem error |
| 130 | interrupted |
Shell completion
eval "$(_GIT_FTP_COMPLETE=bash_source git-ftp)" # zsh: zsh_source, fish: fish_source
Development
uv sync --all-groups
make lint typecheck test # ruff, mypy, pytest (in-process FTP/FTPS/SFTP servers)
make test-docker # pure-ftpd containers, Linux only
The manual page source is docs/git-ftp.1.md (make man renders it with
pandoc).
Releasing
The version is derived from the Git tag by hatch-vcs; there is no version
string to edit. Pushing a v* tag runs .github/workflows/publish.yml, which
builds the sdist and wheel and publishes them to PyPI via Trusted Publishing (no
API token), then creates a GitHub release whose notes are the matching
CHANGELOG.md section.
One-time setup: on PyPI (and TestPyPI) add a Trusted Publisher for this
repository with workflow publish.yml and environment pypi (testpypi), and
create those two environments in the GitHub repository settings.
To cut a release:
# 1. Move the entries under "## [Unreleased]" into a new "## [X.Y.Z]" section
# in CHANGELOG.md, then commit.
$EDITOR CHANGELOG.md
git add CHANGELOG.md
git commit -m "Release X.Y.Z"
# 2. Tag and push. The tag must be v<version>; the workflow checks that it
# matches the version hatch-vcs computes.
git tag -a -m "Release X.Y.Z" vX.Y.Z
git push origin main vX.Y.Z
Between tags, builds report a development version such as X.Y.Z.devN+g<hash>.
To rehearse against TestPyPI without tagging, run the publish workflow manually
(workflow_dispatch) with the TestPyPI option enabled.
License
GPL-3.0-or-later
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_ftp-2.0.0.dev2.tar.gz.
File metadata
- Download URL: git_ftp-2.0.0.dev2.tar.gz
- Upload date:
- Size: 100.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
830c6f82c5144174d726373b27eced6a632c580dc5811d2eada83f31e5a2d887
|
|
| MD5 |
9714ef189b284e11f2429c01c01aa876
|
|
| BLAKE2b-256 |
4be8a97ce92b49e68bf333248c9a4bc4a1768266d0b6fae7a117169c4350e8d2
|
Provenance
The following attestation bundles were made for git_ftp-2.0.0.dev2.tar.gz:
Publisher:
publish.yml on git-ftp/git-ftp-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_ftp-2.0.0.dev2.tar.gz -
Subject digest:
830c6f82c5144174d726373b27eced6a632c580dc5811d2eada83f31e5a2d887 - Sigstore transparency entry: 2830568072
- Sigstore integration time:
-
Permalink:
git-ftp/git-ftp-py@447b91e015bace22df2126270b69d4a58a431fc6 -
Branch / Tag:
refs/tags/v2.0.0.dev2 - Owner: https://github.com/git-ftp
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@447b91e015bace22df2126270b69d4a58a431fc6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file git_ftp-2.0.0.dev2-py3-none-any.whl.
File metadata
- Download URL: git_ftp-2.0.0.dev2-py3-none-any.whl
- Upload date:
- Size: 76.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5b2f28fb6374a8ad61b4be4831c4e202c53e9b019cd6c69f08d4ddf58be27b11
|
|
| MD5 |
05eaff21e73f334d84c776da3401f446
|
|
| BLAKE2b-256 |
bde5bfe4524b79ad73b705fad14e18488c008c7205536c5c678f1897bca29489
|
Provenance
The following attestation bundles were made for git_ftp-2.0.0.dev2-py3-none-any.whl:
Publisher:
publish.yml on git-ftp/git-ftp-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
git_ftp-2.0.0.dev2-py3-none-any.whl -
Subject digest:
5b2f28fb6374a8ad61b4be4831c4e202c53e9b019cd6c69f08d4ddf58be27b11 - Sigstore transparency entry: 2830568109
- Sigstore integration time:
-
Permalink:
git-ftp/git-ftp-py@447b91e015bace22df2126270b69d4a58a431fc6 -
Branch / Tag:
refs/tags/v2.0.0.dev2 - Owner: https://github.com/git-ftp
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@447b91e015bace22df2126270b69d4a58a431fc6 -
Trigger Event:
push
-
Statement type: