Repository: ezyang/git-ftp
Branch: master
Commit: 1c32bcce6991
Files: 7
Total size: 29.6 KB
Directory structure:
gitextract_dsvagi33/
├── .gitignore
├── Makefile
├── README.md
├── git-ftp-test.py
├── git-ftp.1
├── git-ftp.py
└── post-receive
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
================================================
FILE: Makefile
================================================
MANDIR = $(DESTDIR)/usr/share/man/man1
BINDIR = $(DESTDIR)/usr/bin
gitpython:
echo "from git import __version__\nfrom distutils.version import LooseVersion\nif LooseVersion(__version__) < '0.3.0':\n\traise ImportError('gitpython 0.3.x required.')" | python
.PHONY: install
install: gitpython
mkdir -p $(MANDIR)
mkdir -p $(BINDIR)
cp git-ftp.py $(BINDIR)/git-ftp
cp git-ftp.1 $(MANDIR)/git-ftp.1
gzip -f $(MANDIR)/git-ftp.1
.PHONY: uninstall
uninstall:
rm -f $(BINDIR)/git-ftp
rm -f $(MANDIR)/git-ftp.1.gz
================================================
FILE: README.md
================================================
git-ftp.py: quick and efficient publishing of Git repositories over FTP
=======================================================================
Introduction
------------
Some web hosts only give you FTP access to the hosting space, but
you would still like to use Git to version the contents of your
directory. You could upload a full tarball of your website every
time you update but that's wasteful. git-ftp.py only uploads the
files that changed.
Requirements: [git-python 0.3.x](http://gitorious.org/git-python)
it can be installed with `easy_install gitpython`
We also [have a PPA](https://launchpad.net/~niklas-fiekas/+archive/ppa)
which you can install with `sudo add-apt-repository ppa:niklas-fiekas/ppa`
and then `sudo aptitude install git-ftp`.
Usage: `python git-ftp.py`
Note: If you run git-ftp.py for the first time on an existing project
you should upload to the hosting server a `git-rev.txt` file containing
SHA1 of the last commit which is already present there. Otherwise git-ftp.py
will upload and overwite the whole project which is not necessary.
Storing the FTP credentials
---------------------------
You can place FTP credentials in `.git/ftpdata`, as such:
[master]
username=me
password=s00perP4zzw0rd
hostname=ftp.hostname.com
remotepath=/htdocs
ssl=yes
[staging]
username=me
password=s00perP4zzw0rd
hostname=ftp.hostname.com
remotepath=/htdocs/staging
ssl=no
Each section corresponds to a git branch. FTP SSL support needs Python
2.7 or later.
Exluding certain files from uploading
-------------------------------------
Similarly to `.gitignore` you can specify files which you do not wish to upload.
The default file with ignore patterns is `.gitftpignore` in project root directory,
however you can specify your own for every branch in .git/ftpdata:
[branch]
... credentials ...
gitftpignore=.my_gitftpignore
Used syntax is same as .gitignore's with the exception of overriding patterns,
eg. `**!**some/pattern`, which is not supported
Negations within patterns works as expected.
Using a bare repository as a proxy
----------------------------------
An additional script post-receive is provided to allow a central bare repository
to act as a proxy between the git users and the ftp server.
Pushing on branches that don't have an entry in the `ftpdata` configuration file
will have the default git behavior (`git-ftp.py` doesn't get called).
One advantage is that **users do not get to know the ftp credentials** (perfect for interns).
This is how the workflow looks like:
User1 --+ +--> FTP_staging
\ /
User2 -----> Git bare repository -----> FTP_master
/ \
User3 --+ +--> FTP_dev
This is how the setup looks like (One `ftpdata` configuration file, and a symlink to the update hook):
root@server:/path-to-repo/repo.git# ls
HEAD ORIG_HEAD branches config description ftpdata hooks info objects packed-refs refs
root@server:/path-to-repo/repo.git# ls hooks -l
total 0
lrwxr-xr-x 1 root root 29 Aug 19 17:17 post-receive -> /path-to-git-ftp/post-receive
License
--------
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: git-ftp-test.py
================================================
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
import unittest
git_ftp = __import__('git-ftp', globals(), locals(), ['parse_ftpignore', 'is_ignored', 'split_pattern'], 0)
parse_ftpignore = git_ftp.parse_ftpignore
is_ignored = git_ftp.is_ignored
split_pattern = git_ftp.split_pattern
class TestGitFtp(unittest.TestCase):
def test_parse_ftpignore(self):
patterns = '''
# comment and blank line
# negate patterns behaviour (not supported)
!fileX.txt
# directory match
config/
# shell glob (without /)
*swp
BasePresenter.php
# with /
css/*less
# beginning of path
/.htaccess
'''
self.assertEqual(parse_ftpignore(patterns.split("\n")),
['!fileX.txt', 'config/', '*swp', 'BasePresenter.php', 'css/*less', '/.htaccess']
)
pass
def test_split_pattern(self):
self.assertEqual(split_pattern('/foo/rand[/]om/dir/'), ['', 'foo\\Z(?ms)', 'rand[/]om\\Z(?ms)', 'dir\\Z(?ms)', '\\Z(?ms)'])
self.assertEqual(split_pattern('/ano[/]her/bar/file[.-0]txt'), ['', 'ano[/]her\\Z(?ms)', 'bar\\Z(?ms)', 'file[.-0]txt\\Z(?ms)'])
self.assertEqual(split_pattern('left[/right'), ['left\\[\\Z(?ms)', 'right\\Z(?ms)'])
self.assertEqual(split_pattern('left[/notright]'), ['left[/notright]\\Z(?ms)'])
pass
def test_is_ignored(self):
self.assertTrue(is_ignored('/foo/bar/', 'bar/'), 'Ending slash matches only dir.')
self.assertFalse(is_ignored('/foo/bar', 'bar/'), 'Ending slash matches only dir.')
self.assertTrue(is_ignored('/foo/bar/baz', 'bar/'), 'Ending slash matches only dir and path underneath it.')
self.assertFalse(is_ignored('foo/bar', 'foo?*bar'), 'Slash must be matched explicitly.')
self.assertTrue(is_ignored('/foo/bar/', 'bar'))
self.assertTrue(is_ignored('/foo/bar', 'bar'))
self.assertTrue(is_ignored('/foo/bar/baz', 'bar'))
self.assertTrue(is_ignored('/foo/bar/file.txt', 'bar/*.txt'))
self.assertFalse(is_ignored('/foo/bar/file.txt', '/*.txt'), 'Leading slash matches against root dir.')
self.assertTrue(is_ignored('/file.txt', '/*.txt'), 'Leading slash matches against root dir.')
self.assertTrue(is_ignored('/foo/bar/output.o', 'bar/*.[oa]'), 'Character group.')
self.assertFalse(is_ignored('/aaa/bbb/ccc', 'aaa/[!b]*'), 'Character ignore.')
self.assertTrue(is_ignored('/aaa/bbb/ccc', '[a-z][a-c][!b-d]'), 'Character range.')
pass
if __name__ == '__main__':
unittest.main()
================================================
FILE: git-ftp.1
================================================
.TH GIT\-FTP 1 18/10/2011 HEAD "Git Manual"
.SH "NAME"
git-ftp \- Quick and efficient publishing of Git repositories over FTP
.SH "SYNOPSIS"
.sp
.nf
\fIgit ftp\fR [(\-\-force | \-f)] [(\-\-quiet | \-q)]
[(\-\-revision | \-r) <commit>] [(\-\-commit | \-c) <commit>]
[(\-\-branch | \-b) <branch>] [(\-\-section | \-s) <section>]
.fi
.sp
.SH "DESCRIPTION"
.sp
Some web hosts only give you FTP access to the hosting space, but you would
still like to use Git to version the contents of your directory. You could
upload a full tarball of your website every time you update, but that's
wasteful. \fIgit ftp\fR only uploads the files that changed.
.SH "OPTIONS"
.PP
\-f, \-\-force
.RS 4
Force the reupload of all files instead of just the changed ones\&.
.RE
.PP
\-q, \-\-quiet
.RS 4
Display only errors and warnings\&.
.RE
.PP
\-r <commit>, \-\-revision=<commit>
.RS 4
The SHA of the current revision is stored in \fIgit-rev.txt\fR on the server.
Use this revision instead of the server stored one, to determine which files
have changed\&.
.RE
.PP
\-c <commit>, \-\-commit=<commit>
.RS 4
Upload this commit instead of HEAD or the tip of the selected branch\&.
.RE
.PP
\-b <branch>, \-\-branch=<branch>
.RS 4
Use this branch instead of the active one\&.
.RE
.PP
\-s <section>, \-\-section=<section>
.RS 4
Use this section of the ftpdata file instead of the active branch name\&.
.RE
.SH "FTP CREDENTIALS"
.sp
You can place FTP credentials in \fI.git/ftpdata\fR, as such:
.sp
.if n \{\
.RS 4
.\}
.nf
[master]
username=me
password=s00perP4zzw0rd
hostname=ftp.hostname.com
remotepath=/htdocs
ssl=yes
[staging]
username=me
password=s00perP4zzw0rd
hostname=ftp.hostname.com
remotepath=/htdocs/staging
ssl=no
.fi
.if n \{\
.RE
.\}
.sp
Each section corresponds to a Git branch. If you don't create the configuration
file, \fIgit ftp\fR will interactively prompt you.
.sp
FTP SSL support needs Python 2.7 or later.
.SH "EXCLUDING FILES FROM UPLOADING"
.sp
Similarly to \fI.gitignore\fR you can exclude files from uploading.
.sp
The default file with ignore patterns is \fI.gitftpignore\fR in project root,
however you can specify your own for every branch in .git/ftpdata:
.sp
.if n\{\
.RS 4
.\}
.nf
[branch]
... credentials ...
gitftpignore=.my_gitftpignore
.fi
.if n\{\
.RE
.\}
.sp
Used syntax is same as gitignore's with the exception of overriding patterns,
eg. "\fI!\fRsome/pattern", which is not supported.
Negations within patterns works as expected.
.SH "USING A BARE REPOSITORY AS A PROXY"
.sp
An additional script \fIpost-recieve\fR is provided to allow a central bare
repository to act as a proxy between the git users and the ftp server.
.sp
Pusing on branches that don't have an entry in the \fIftpdata\fR configuration file will have the default Git behaviour - nothing will be pushed over ftp.
.sp
One advantage is that users do not get to know the ftp credentials (perfect for
interns).
.sp
This is how the workflow looks like:
.sp
.if n \{\
.RS 4
.\}
.nf
User 1 --+ +--> FTP Staging
\\ /
User 2 -------> Bare Git repository -------> FTP Master
/ \\
User 3 --+ +--> FTP Dev
.fi
.if n \{\
.RE
.\}
.sp
This is how the setup looks like (one \fIftpdata\fR configuration file and a
symlink to the update hook):
.sp
.if n \{\
.RS 4
.\}
.nf
user@server:/path-to-repo/repo.git$ ls
HEAD ORIG_HEAD branches config description ftpdata hooks info
user@server:/path-to-repo/repo.git/hooks$ ls -l
lrwxr-xr-x 1 user user post-recieve -> /path-to-git-ftp/post-recieve
.fi
.if n \{\
.RE
.\}
.SH "LICENSE"
.sp
Copyright (c) 2008 - 2011
Edward Z. Yang <ezyang@mit.edu>, Mauro Lizaur <mauro@cacavoladora.org> and
Niklas Fiekas <niklas.fiekas@googlemail.com>
.sp
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
.sp
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
.sp
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
.SH "REPORTING BUGS"
Report bugs in the issue queue on Github
<https://github.com/ezyang/git-ftp/issues> or email one of the authors.
.SH "GIT"
.sp
Used as a part of the \fBgit\fR(1) suite.
================================================
FILE: git-ftp.py
================================================
#!/usr/bin/env python
"""
git-ftp: painless, quick and easy working copy syncing over FTP
Copyright (c) 2008-2012
Edward Z. Yang <ezyang@mit.edu>, Mauro Lizaur <mauro@cacavoladora.org> and
Niklas Fiekas <niklas.fiekas@googlemail.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
import ftplib
import re
import sys
import os.path
import posixpath # use this for ftp manipulation
import getpass
import optparse
import logging
import textwrap
import fnmatch
from io import BytesIO
try:
import configparser as ConfigParser
except ImportError:
import ConfigParser
# Note about Tree.path/Blob.path: *real* Git trees and blobs don't
# actually provide path information, but the git-python bindings, as a
# convenience keep track of this if you access the blob from an index.
# This ends up considerably simplifying our code, but do be careful!
from distutils.version import LooseVersion
from git import __version__ as git_version
if LooseVersion(git_version) < '0.3.0':
print('git-ftp requires git-python 0.3.0 or newer; %s provided.' % git_version)
exit(1)
from git import Blob, Repo, Git, Submodule
class BranchNotFound(Exception):
pass
class FtpDataOldVersion(Exception):
pass
class FtpSslNotSupported(Exception):
pass
class SectionNotFound(Exception):
pass
def split_pattern(path): # TODO: Improve skeevy code
path = fnmatch.translate(path).split('\\/')
for i, p in enumerate(path[:-1]):
if p:
path[i] = p + '\\Z(?ms)'
return path
# ezyang: This code is pretty skeevy; there is probably a better,
# more obviously correct way of spelling it. Refactor me...
def is_ignored(path, regex):
regex = split_pattern(os.path.normcase(regex))
path = os.path.normcase(path).split('/')
regex_pos = path_pos = 0
if regex[0] == '': # leading slash - root dir must match
if path[0] != '' or not re.match(regex[1], path[1]):
return False
regex_pos = path_pos = 2
if not regex_pos: # find beginning of regex
for i, p in enumerate(path):
if re.match(regex[0], p):
regex_pos = 1
path_pos = i + 1
break
else:
return False
if len(path[path_pos:]) < len(regex[regex_pos:]):
return False
n = len(regex)
for r in regex[regex_pos:]: # match the rest
if regex_pos + 1 == n: # last item; if empty match anything
if re.match(r, ''):
return True
if not re.match(r, path[path_pos]):
return False
path_pos += 1
regex_pos += 1
return True
def main():
Git.git_binary = 'git' # Windows doesn't like env
repo, options, args = parse_args()
if repo.is_dirty() and not options.commit:
logging.warning("Working copy is dirty; uncommitted changes will NOT be uploaded")
base = options.ftp.remotepath
logging.info("Base directory is %s", base)
try:
branch = next(h for h in repo.heads if h.name == options.branch)
except StopIteration:
raise BranchNotFound
commit = branch.commit
if options.commit:
commit = repo.commit(options.commit)
tree = commit.tree
if options.ftp.ssl:
if hasattr(ftplib, 'FTP_TLS'): # SSL new in 2.7+
ftp = ftplib.FTP_TLS(options.ftp.hostname, options.ftp.username, options.ftp.password)
ftp.prot_p()
logging.info("Using SSL")
else:
raise FtpSslNotSupported("Python is too old for FTP SSL. Try using Python 2.7 or later.")
else:
ftp = ftplib.FTP(options.ftp.hostname, options.ftp.username, options.ftp.password)
ftp.cwd(base)
# Check revision
hash = options.revision
if not options.force and not hash:
hashFile = BytesIO()
try:
ftp.retrbinary('RETR git-rev.txt', hashFile.write)
hash = hashFile.getvalue().strip()
except ftplib.error_perm:
pass
# Load ftpignore rules, if any
patterns = []
gitftpignore = os.path.join(repo.working_dir, options.ftp.gitftpignore)
if os.path.isfile(gitftpignore):
with open(gitftpignore, 'r') as ftpignore:
patterns = parse_ftpignore(ftpignore)
patterns.append('/' + options.ftp.gitftpignore)
if not hash:
# Diffing against an empty tree will cause a full upload.
oldtree = get_empty_tree(repo)
else:
oldtree = repo.commit(hash).tree
if oldtree.hexsha == tree.hexsha:
logging.info('Nothing to do!')
else:
upload_diff(repo, oldtree, tree, ftp, [base], patterns)
ftp.storbinary('STOR git-rev.txt', BytesIO(commit.hexsha.encode('utf-8')))
ftp.quit()
def parse_ftpignore(rawPatterns):
patterns = []
for pat in rawPatterns:
pat = pat.rstrip()
if not pat or pat.startswith('#'):
continue
patterns.append(pat)
return patterns
def parse_args():
usage = 'usage: %prog [OPTIONS] [DIRECTORY]'
desc = """\
This script uploads files in a Git repository to a
website via FTP, but is smart and only uploads file
that have changed.
"""
parser = optparse.OptionParser(usage, description=textwrap.dedent(desc))
parser.add_option('-f', '--force', dest="force", action="store_true", default=False,
help="force the reupload of all files")
parser.add_option('-q', '--quiet', dest="quiet", action="store_true", default=False,
help="quiet output")
parser.add_option('-r', '--revision', dest="revision", default=None,
help="use this revision instead of the server stored one")
parser.add_option('-b', '--branch', dest="branch", default=None,
help="use this branch instead of the active one")
parser.add_option('-c', '--commit', dest="commit", default=None,
help="use this commit instead of HEAD")
parser.add_option('-s', '--section', dest="section", default=None,
help="use this section from ftpdata instead of branch name")
options, args = parser.parse_args()
configure_logging(options)
if len(args) > 1:
parser.error("too many arguments")
if args:
cwd = args[0]
else:
cwd = "."
repo = Repo(cwd)
if not options.branch:
options.branch = repo.active_branch.name
if not options.section:
options.section = options.branch
get_ftp_creds(repo, options)
return repo, options, args
def configure_logging(options):
logger = logging.getLogger()
if not options.quiet:
logger.setLevel(logging.INFO)
ch = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter("%(levelname)s: %(message)s")
ch.setFormatter(formatter)
logger.addHandler(ch)
def format_mode(mode):
return "%o" % (mode & 0o777)
class FtpData():
password = None
username = None
hostname = None
remotepath = None
ssl = None
gitftpignore = None
def get_ftp_creds(repo, options):
"""
Retrieves the data to connect to the FTP from .git/ftpdata
or interactively.
ftpdata format example:
[branch]
username=me
password=s00perP4zzw0rd
hostname=ftp.hostname.com
remotepath=/htdocs
ssl=yes
gitftpignore=.gitftpignore
Please note that it isn't necessary to have this file,
you'll be asked for the data every time you upload something.
"""
ftpdata = os.path.join(repo.git_dir, "ftpdata")
options.ftp = FtpData()
cfg = ConfigParser.ConfigParser()
if os.path.isfile(ftpdata):
logging.info("Using .git/ftpdata")
cfg.read(ftpdata)
if (not cfg.has_section(options.section)):
if cfg.has_section('ftp'):
raise FtpDataOldVersion("Please rename the [ftp] section to [branch]. " +
"Take a look at the README for more information")
else:
raise SectionNotFound("Your .git/ftpdata file does not contain a section " +
"named '%s'" % options.section)
# just in case you do not want to store your ftp password.
try:
options.ftp.password = cfg.get(options.section, 'password')
except ConfigParser.NoOptionError:
options.ftp.password = getpass.getpass('FTP Password: ')
options.ftp.username = cfg.get(options.section, 'username')
options.ftp.hostname = cfg.get(options.section, 'hostname')
options.ftp.remotepath = cfg.get(options.section, 'remotepath')
try:
options.ftp.ssl = boolish(cfg.get(options.section, 'ssl'))
except ConfigParser.NoOptionError:
options.ftp.ssl = False
try:
options.ftp.gitftpignore = cfg.get(options.section, 'gitftpignore')
except ConfigParser.NoOptionError:
options.ftp.gitftpignore = '.gitftpignore'
else:
print("Please configure settings for branch '%s'" % options.section)
options.ftp.username = raw_input('FTP Username: ')
options.ftp.password = getpass.getpass('FTP Password: ')
options.ftp.hostname = raw_input('FTP Hostname: ')
options.ftp.remotepath = raw_input('Remote Path: ')
if hasattr(ftplib, 'FTP_TLS'):
options.ftp.ssl = ask_ok('Use SSL? ')
else:
logging.warning("SSL not supported, defaulting to no")
# set default branch
if ask_ok("Should I write ftp details to .git/ftpdata? "):
cfg.add_section(options.section)
cfg.set(options.section, 'username', options.ftp.username)
cfg.set(options.section, 'password', options.ftp.password)
cfg.set(options.section, 'hostname', options.ftp.hostname)
cfg.set(options.section, 'remotepath', options.ftp.remotepath)
cfg.set(options.section, 'ssl', options.ftp.ssl)
f = open(ftpdata, 'w')
cfg.write(f)
def get_empty_tree(repo):
return repo.tree(repo.git.hash_object('-w', '-t', 'tree', os.devnull))
def upload_diff(repo, oldtree, tree, ftp, base, ignored):
"""
Upload and/or delete items according to a Git diff between two trees.
upload_diff requires, that the ftp working directory is set to the base
of the current repository before it is called.
Keyword arguments:
repo -- The git.Repo to upload objects from
oldtree -- The old tree to diff against. An empty tree will cause a full
upload of the new tree.
tree -- The new tree. An empty tree will cause a full removal of all
objects of the old tree.
ftp -- The active ftplib.FTP object to upload contents to
base -- The list of base directory and submodule paths to upload contents
to in ftp.
For example, base = ['www', 'www']. base must exist and must not
have a trailing slash.
ignored -- The list of patterns explicitly ignored by gitftpignore.
"""
# -z is used so we don't have to deal with quotes in path matching
diff = repo.git.diff("--name-status", "--no-renames", "-z", oldtree.hexsha, tree.hexsha)
diff = iter(diff.split("\0"))
for line in diff:
if not line:
continue
status, file = line, next(diff)
assert status in ['A', 'D', 'M']
filepath = posixpath.join(*(['/'] + base[1:] + [file]))
if is_ignored_path(filepath, ignored):
logging.info('Skipped ' + filepath)
continue
if status == "D":
try:
ftp.delete(file)
logging.info('Deleted ' + file)
except ftplib.error_perm:
logging.warning('Failed to delete ' + file)
# Now let's see if we need to remove some subdirectories
def generate_parent_dirs(x):
# invariant: x is a filename
while '/' in x:
x = posixpath.dirname(x)
yield x
for dir in generate_parent_dirs(file):
try:
# unfortunately, dir in tree doesn't work for subdirs
tree[dir]
except KeyError:
try:
ftp.rmd(dir)
logging.debug('Cleaned away ' + dir)
except ftplib.error_perm:
logging.info('Did not clean away ' + dir)
break
else:
node = tree[file]
if status == "A":
# try building up the parent directory
subtree = tree
if isinstance(node, Blob):
directories = file.split("/")[:-1]
else:
# for submodules also add the directory itself
assert isinstance(node, Submodule)
directories = file.split("/")
for c in directories:
subtree = subtree / c
try:
ftp.mkd(subtree.path)
except ftplib.error_perm:
pass
if isinstance(node, Blob):
upload_blob(node, ftp)
else:
module = node.module()
module_tree = module.commit(node.hexsha).tree
if status == "A":
module_oldtree = get_empty_tree(module)
else:
oldnode = oldtree[file]
assert isinstance(oldnode, Submodule) # TODO: What if not?
module_oldtree = module.commit(oldnode.hexsha).tree
module_base = base + [node.path]
logging.info('Entering submodule %s', node.path)
ftp.cwd(posixpath.join(*module_base))
upload_diff(module, module_oldtree, module_tree, ftp, module_base, ignored)
logging.info('Leaving submodule %s', node.path)
ftp.cwd(posixpath.join(*base))
def is_ignored_path(path, patterns, quiet=False):
"""Returns true if a filepath is ignored by gitftpignore."""
if is_special_file(path):
return True
for pat in patterns:
if is_ignored(path, pat):
return True
return False
def is_special_file(name):
"""Returns true if a file is some special Git metadata and not content."""
return posixpath.basename(name) in ['.gitignore', '.gitattributes', '.gitmodules']
def upload_blob(blob, ftp, quiet=False):
"""
Uploads a blob. Pre-condition on ftp is that our current working
directory is the root directory of the repository being uploaded
(that means DON'T use ftp.cwd; we'll use full paths appropriately).
"""
if not quiet:
logging.info('Uploading ' + blob.path)
try:
ftp.delete(blob.path)
except ftplib.error_perm:
pass
ftp.storbinary('STOR ' + blob.path, blob.data_stream)
try:
ftp.voidcmd('SITE CHMOD ' + format_mode(blob.mode) + ' ' + blob.path)
except ftplib.error_perm:
# Ignore Windows chmod errors
logging.warning('Failed to chmod ' + blob.path)
pass
def boolish(s):
if s in ('1', 'true', 'y', 'ye', 'yes', 'on'):
return True
if s in ('0', 'false', 'n', 'no', 'off'):
return False
return None
def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
while True:
ok = raw_input(prompt).lower()
r = boolish(ok)
if r is not None:
return r
retries = retries - 1
if retries < 0:
raise IOError('Wrong user input.')
print(complaint)
if __name__ == "__main__":
main()
================================================
FILE: post-receive
================================================
#!/bin/bash
# You may install this post-receive hook in your remote git repository
# to have automatic file upload when pushing to the repository.
while read OLD_COMMIT NEW_COMMIT REFNAME; do
BRANCH=${REFNAME#refs/heads/}
if [[ `grep "^\[$BRANCH\]$" ftpdata` ]]; then
echo "Uploading $BRANCH..."
$(dirname $(readlink -f "$0"))/git-ftp.py -b "$BRANCH" -c "$NEW_COMMIT" || exit $?
fi
done
true
gitextract_dsvagi33/ ├── .gitignore ├── Makefile ├── README.md ├── git-ftp-test.py ├── git-ftp.1 ├── git-ftp.py └── post-receive
SYMBOL INDEX (24 symbols across 2 files)
FILE: git-ftp-test.py
class TestGitFtp (line 12) | class TestGitFtp(unittest.TestCase):
method test_parse_ftpignore (line 14) | def test_parse_ftpignore(self):
method test_split_pattern (line 35) | def test_split_pattern(self):
method test_is_ignored (line 42) | def test_is_ignored(self):
FILE: git-ftp.py
class BranchNotFound (line 64) | class BranchNotFound(Exception):
class FtpDataOldVersion (line 68) | class FtpDataOldVersion(Exception):
class FtpSslNotSupported (line 72) | class FtpSslNotSupported(Exception):
class SectionNotFound (line 76) | class SectionNotFound(Exception):
function split_pattern (line 80) | def split_pattern(path): # TODO: Improve skeevy code
function is_ignored (line 89) | def is_ignored(path, regex):
function main (line 125) | def main():
function parse_ftpignore (line 188) | def parse_ftpignore(rawPatterns):
function parse_args (line 198) | def parse_args():
function configure_logging (line 238) | def configure_logging(options):
function format_mode (line 248) | def format_mode(mode):
class FtpData (line 252) | class FtpData():
function get_ftp_creds (line 261) | def get_ftp_creds(repo, options):
function get_empty_tree (line 336) | def get_empty_tree(repo):
function upload_diff (line 340) | def upload_diff(repo, oldtree, tree, ftp, base, ignored):
function is_ignored_path (line 437) | def is_ignored_path(path, patterns, quiet=False):
function is_special_file (line 447) | def is_special_file(name):
function upload_blob (line 452) | def upload_blob(blob, ftp, quiet=False):
function boolish (line 473) | def boolish(s):
function ask_ok (line 481) | def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
Condensed preview — 7 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (32K chars).
[
{
"path": ".gitignore",
"chars": 725,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "Makefile",
"chars": 516,
"preview": "MANDIR = $(DESTDIR)/usr/share/man/man1\nBINDIR = $(DESTDIR)/usr/bin\n\ngitpython:\n\techo \"from git import __version__\\nfrom "
},
{
"path": "README.md",
"chars": 4289,
"preview": "git-ftp.py: quick and efficient publishing of Git repositories over FTP\n================================================"
},
{
"path": "git-ftp-test.py",
"chars": 2484,
"preview": "#!/usr/bin/env python\n# -*- coding:utf-8 -*-\n#\nimport unittest\n\ngit_ftp = __import__('git-ftp', globals(), locals(), ['p"
},
{
"path": "git-ftp.1",
"chars": 5045,
"preview": ".TH GIT\\-FTP 1 18/10/2011 HEAD \"Git Manual\"\n.SH \"NAME\"\ngit-ftp \\- Quick and efficient publishing of Git repositories ove"
},
{
"path": "git-ftp.py",
"chars": 16807,
"preview": "#!/usr/bin/env python\n\n\"\"\"\ngit-ftp: painless, quick and easy working copy syncing over FTP\n\nCopyright (c) 2008-2012\nEdwa"
},
{
"path": "post-receive",
"chars": 421,
"preview": "#!/bin/bash\n# You may install this post-receive hook in your remote git repository\n# to have automatic file upload when "
}
]
About this extraction
This page contains the full source code of the ezyang/git-ftp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 7 files (29.6 KB), approximately 7.6k tokens, and a symbol index with 24 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.