Full Code of ezyang/git-ftp for AI

master 1c32bcce6991 cached
7 files
29.6 KB
7.6k tokens
24 symbols
1 requests
Download .txt
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
Download .txt
gitextract_dsvagi33/

├── .gitignore
├── Makefile
├── README.md
├── git-ftp-test.py
├── git-ftp.1
├── git-ftp.py
└── post-receive
Download .txt
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.

Copied to clipboard!