[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/"
  },
  {
    "path": "Makefile",
    "content": "MANDIR = $(DESTDIR)/usr/share/man/man1\nBINDIR = $(DESTDIR)/usr/bin\n\ngitpython:\n\techo \"from git import __version__\\nfrom distutils.version import LooseVersion\\nif LooseVersion(__version__) < '0.3.0':\\n\\traise ImportError('gitpython 0.3.x required.')\" | python\n\n.PHONY: install\ninstall: gitpython\n\tmkdir -p $(MANDIR)\n\tmkdir -p $(BINDIR)\n\tcp git-ftp.py $(BINDIR)/git-ftp\n\tcp git-ftp.1 $(MANDIR)/git-ftp.1\n\tgzip -f $(MANDIR)/git-ftp.1\n\n.PHONY: uninstall\nuninstall:\n\trm -f $(BINDIR)/git-ftp\n\trm -f $(MANDIR)/git-ftp.1.gz\n"
  },
  {
    "path": "README.md",
    "content": "git-ftp.py: quick and efficient publishing of Git repositories over FTP\n=======================================================================\n\nIntroduction\n------------\n\nSome web hosts only give you FTP access to the hosting space, but\nyou would still like to use Git to version the contents of your\ndirectory.  You could upload a full tarball of your website every\ntime you update but that's wasteful.  git-ftp.py only uploads the\nfiles that changed.\n\nRequirements: [git-python 0.3.x](http://gitorious.org/git-python)  \nit can be installed with `easy_install gitpython`\n\nWe also [have a PPA](https://launchpad.net/~niklas-fiekas/+archive/ppa)\nwhich you can install with `sudo add-apt-repository ppa:niklas-fiekas/ppa`\nand then `sudo aptitude install git-ftp`.\n\nUsage: `python git-ftp.py`\n\nNote: If you run git-ftp.py for the first time on an existing project \nyou should upload to the hosting server a `git-rev.txt` file containing \nSHA1 of the last commit which is already present there. Otherwise git-ftp.py \nwill upload and overwite the whole project which is not necessary.\n\nStoring the FTP credentials\n---------------------------\n\nYou can place FTP credentials in `.git/ftpdata`, as such:\n\n    [master]\n    username=me\n    password=s00perP4zzw0rd\n    hostname=ftp.hostname.com\n    remotepath=/htdocs\n    ssl=yes\n\n    [staging]\n    username=me\n    password=s00perP4zzw0rd\n    hostname=ftp.hostname.com\n    remotepath=/htdocs/staging\n    ssl=no\n\nEach section corresponds to a git branch. FTP SSL support needs Python\n2.7 or later.\n\nExluding certain files from uploading\n-------------------------------------\n\nSimilarly to `.gitignore` you can specify files which you do not wish to upload.\nThe default file with ignore patterns is `.gitftpignore` in project root directory,\nhowever you can specify your own for every branch in .git/ftpdata:\n\n    [branch]\n    ... credentials ...\n    gitftpignore=.my_gitftpignore\n\nUsed syntax is same as .gitignore's with the exception of overriding patterns,\neg. `**!**some/pattern`, which is not supported\nNegations within patterns works as expected. \n\nUsing a bare repository as a proxy\n----------------------------------\n\nAn additional script post-receive is provided to allow a central bare repository\nto act as a proxy between the git users and the ftp server.  \nPushing on branches that don't have an entry in the `ftpdata` configuration file\nwill have the default git behavior (`git-ftp.py` doesn't get called).\nOne advantage is that **users do not get to know the ftp credentials** (perfect for interns).  \nThis is how the workflow looks like:\n\n    User1 --+                          +--> FTP_staging\n             \\                        /\n    User2 -----> Git bare repository -----> FTP_master\n             /                        \\\n    User3 --+                          +--> FTP_dev\n\nThis is how the setup looks like (One `ftpdata` configuration file, and a symlink to the update hook):\n\n    root@server:/path-to-repo/repo.git# ls\n    HEAD  ORIG_HEAD  branches  config  description  ftpdata  hooks  info  objects  packed-refs  refs\n    root@server:/path-to-repo/repo.git# ls hooks -l\n    total 0\n    lrwxr-xr-x 1 root    root      29 Aug 19 17:17 post-receive -> /path-to-git-ftp/post-receive\n\n\nLicense\n--------\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "git-ftp-test.py",
    "content": "#!/usr/bin/env python\n# -*- coding:utf-8 -*-\n#\nimport unittest\n\ngit_ftp = __import__('git-ftp', globals(), locals(), ['parse_ftpignore', 'is_ignored', 'split_pattern'], 0)\nparse_ftpignore = git_ftp.parse_ftpignore\nis_ignored = git_ftp.is_ignored\nsplit_pattern = git_ftp.split_pattern\n\n\nclass TestGitFtp(unittest.TestCase):\n\n    def test_parse_ftpignore(self):\n        patterns = '''\n# comment and blank line\n\n# negate patterns behaviour (not supported)\n!fileX.txt\n# directory match\nconfig/\n# shell glob (without /)\n*swp\nBasePresenter.php\n# with /\ncss/*less\n# beginning of path\n/.htaccess\n        '''\n        self.assertEqual(parse_ftpignore(patterns.split(\"\\n\")),\n            ['!fileX.txt', 'config/', '*swp', 'BasePresenter.php', 'css/*less', '/.htaccess']\n        )\n    pass\n\n    def test_split_pattern(self):\n        self.assertEqual(split_pattern('/foo/rand[/]om/dir/'), ['', 'foo\\\\Z(?ms)', 'rand[/]om\\\\Z(?ms)', 'dir\\\\Z(?ms)', '\\\\Z(?ms)'])\n        self.assertEqual(split_pattern('/ano[/]her/bar/file[.-0]txt'), ['', 'ano[/]her\\\\Z(?ms)', 'bar\\\\Z(?ms)', 'file[.-0]txt\\\\Z(?ms)'])\n        self.assertEqual(split_pattern('left[/right'), ['left\\\\[\\\\Z(?ms)', 'right\\\\Z(?ms)'])\n        self.assertEqual(split_pattern('left[/notright]'), ['left[/notright]\\\\Z(?ms)'])\n    pass\n\n    def test_is_ignored(self):\n        self.assertTrue(is_ignored('/foo/bar/', 'bar/'), 'Ending slash matches only dir.')\n        self.assertFalse(is_ignored('/foo/bar', 'bar/'), 'Ending slash matches only dir.')\n        self.assertTrue(is_ignored('/foo/bar/baz', 'bar/'), 'Ending slash matches only dir and path underneath it.')\n\n        self.assertFalse(is_ignored('foo/bar', 'foo?*bar'), 'Slash must be matched explicitly.')\n\n        self.assertTrue(is_ignored('/foo/bar/', 'bar'))\n        self.assertTrue(is_ignored('/foo/bar', 'bar'))\n        self.assertTrue(is_ignored('/foo/bar/baz', 'bar'))\n\n        self.assertTrue(is_ignored('/foo/bar/file.txt', 'bar/*.txt'))\n        self.assertFalse(is_ignored('/foo/bar/file.txt', '/*.txt'), 'Leading slash matches against root dir.')\n        self.assertTrue(is_ignored('/file.txt', '/*.txt'), 'Leading slash matches against root dir.')\n\n        self.assertTrue(is_ignored('/foo/bar/output.o', 'bar/*.[oa]'), 'Character group.')\n        self.assertFalse(is_ignored('/aaa/bbb/ccc', 'aaa/[!b]*'), 'Character ignore.')\n        self.assertTrue(is_ignored('/aaa/bbb/ccc', '[a-z][a-c][!b-d]'), 'Character range.')\n    pass\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "git-ftp.1",
    "content": ".TH GIT\\-FTP 1 18/10/2011 HEAD \"Git Manual\"\n.SH \"NAME\"\ngit-ftp \\- Quick and efficient publishing of Git repositories over FTP\n\n\n.SH \"SYNOPSIS\"\n.sp\n.nf\n\\fIgit ftp\\fR [(\\-\\-force | \\-f)] [(\\-\\-quiet | \\-q)]\n        [(\\-\\-revision | \\-r) <commit>] [(\\-\\-commit | \\-c) <commit>]\n        [(\\-\\-branch | \\-b) <branch>] [(\\-\\-section | \\-s) <section>]\n.fi\n.sp\n\n\n.SH \"DESCRIPTION\"\n.sp\nSome web hosts only give you FTP access to the hosting space, but you would\nstill like to use Git to version the contents of your directory. You could\nupload a full tarball of your website every time you update, but that's\nwasteful. \\fIgit ftp\\fR only uploads the files that changed.\n\n\n.SH \"OPTIONS\"\n\n.PP\n\\-f, \\-\\-force\n.RS 4\nForce the reupload of all files instead of just the changed ones\\&.\n.RE\n\n.PP\n\\-q, \\-\\-quiet\n.RS 4\nDisplay only errors and warnings\\&.\n.RE\n\n.PP\n\\-r <commit>, \\-\\-revision=<commit>\n.RS 4\nThe SHA of the current revision is stored in \\fIgit-rev.txt\\fR on the server.\nUse this revision instead of the server stored one, to determine which files\nhave changed\\&.\n.RE\n\n.PP\n\\-c <commit>, \\-\\-commit=<commit>\n.RS 4\nUpload this commit instead of HEAD or the tip of the selected branch\\&.\n.RE\n\n.PP\n\\-b <branch>, \\-\\-branch=<branch>\n.RS 4\nUse this branch instead of the active one\\&.\n.RE\n\n.PP\n\\-s <section>, \\-\\-section=<section>\n.RS 4\nUse this section of the ftpdata file instead of the active branch name\\&.\n.RE\n\n.SH \"FTP CREDENTIALS\"\n.sp\nYou can place FTP credentials in \\fI.git/ftpdata\\fR, as such:\n.sp\n.if n \\{\\\n.RS 4\n.\\}\n.nf\n[master]\nusername=me\npassword=s00perP4zzw0rd\nhostname=ftp.hostname.com\nremotepath=/htdocs\nssl=yes\n\n[staging]\nusername=me\npassword=s00perP4zzw0rd\nhostname=ftp.hostname.com\nremotepath=/htdocs/staging\nssl=no\n.fi\n.if n \\{\\\n.RE\n.\\}\n.sp\nEach section corresponds to a Git branch. If you don't create the configuration\nfile, \\fIgit ftp\\fR will interactively prompt you.\n.sp\nFTP SSL support needs Python 2.7 or later.\n\n\n.SH \"EXCLUDING FILES FROM UPLOADING\"\n.sp\nSimilarly to \\fI.gitignore\\fR you can exclude files from uploading.\n.sp\nThe default file with ignore patterns is \\fI.gitftpignore\\fR in project root,\nhowever you can specify your own for every branch in .git/ftpdata:\n.sp\n.if n\\{\\\n.RS 4\n.\\}\n.nf\n[branch]\n ... credentials ...\ngitftpignore=.my_gitftpignore\n.fi\n.if n\\{\\\n.RE\n.\\}\n.sp\nUsed syntax is same as gitignore's with the exception of overriding patterns,\neg. \"\\fI!\\fRsome/pattern\", which is not supported.\nNegations within patterns works as expected.\n\n\n.SH \"USING A BARE REPOSITORY AS A PROXY\"\n.sp\nAn additional script \\fIpost-recieve\\fR is provided to allow a central bare\nrepository to act as a proxy between the git users and the ftp server.\n.sp\nPusing 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.\n.sp\nOne advantage is that users do not get to know the ftp credentials (perfect for\ninterns).\n.sp\nThis is how the workflow looks like:\n.sp\n.if n \\{\\\n.RS 4\n.\\}\n.nf\nUser 1 --+                              +--> FTP Staging\n          \\\\                            /\nUser 2 -------> Bare Git repository -------> FTP Master\n          /                            \\\\\nUser 3 --+                              +--> FTP Dev\n.fi\n.if n \\{\\\n.RE\n.\\}\n.sp\nThis is how the setup looks like (one \\fIftpdata\\fR configuration file and a\nsymlink to the update hook):\n.sp\n.if n \\{\\\n.RS 4\n.\\}\n.nf\nuser@server:/path-to-repo/repo.git$ ls\nHEAD  ORIG_HEAD  branches  config  description  ftpdata  hooks  info\n\nuser@server:/path-to-repo/repo.git/hooks$ ls -l\nlrwxr-xr-x 1  user user  post-recieve -> /path-to-git-ftp/post-recieve\n.fi\n.if n \\{\\\n.RE\n.\\}\n\n\n.SH \"LICENSE\"\n.sp\nCopyright (c) 2008 - 2011\nEdward Z. Yang <ezyang@mit.edu>, Mauro Lizaur <mauro@cacavoladora.org> and\nNiklas Fiekas <niklas.fiekas@googlemail.com>\n.sp\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n.sp\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n.sp\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\n\n.SH \"REPORTING BUGS\"\nReport bugs in the issue queue on Github\n<https://github.com/ezyang/git-ftp/issues> or email one of the authors.\n\n\n.SH \"GIT\"\n.sp\nUsed as a part of the \\fBgit\\fR(1) suite.\n"
  },
  {
    "path": "git-ftp.py",
    "content": "#!/usr/bin/env python\n\n\"\"\"\ngit-ftp: painless, quick and easy working copy syncing over FTP\n\nCopyright (c) 2008-2012\nEdward Z. Yang <ezyang@mit.edu>, Mauro Lizaur <mauro@cacavoladora.org> and\nNiklas Fiekas <niklas.fiekas@googlemail.com>\n\nPermission is hereby granted, free of charge, to any person\nobtaining a copy of this software and associated documentation\nfiles (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use,\ncopy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\"\"\"\n\nimport ftplib\nimport re\nimport sys\nimport os.path\nimport posixpath  # use this for ftp manipulation\nimport getpass\nimport optparse\nimport logging\nimport textwrap\nimport fnmatch\nfrom io import BytesIO\ntry:\n    import configparser as ConfigParser\nexcept ImportError:\n    import ConfigParser\n\n\n# Note about Tree.path/Blob.path: *real* Git trees and blobs don't\n# actually provide path information, but the git-python bindings, as a\n# convenience keep track of this if you access the blob from an index.\n# This ends up considerably simplifying our code, but do be careful!\n\nfrom distutils.version import LooseVersion\nfrom git import __version__ as git_version\n\nif LooseVersion(git_version) < '0.3.0':\n    print('git-ftp requires git-python 0.3.0 or newer; %s provided.' % git_version)\n    exit(1)\n\nfrom git import Blob, Repo, Git, Submodule\n\n\nclass BranchNotFound(Exception):\n    pass\n\n\nclass FtpDataOldVersion(Exception):\n    pass\n\n\nclass FtpSslNotSupported(Exception):\n    pass\n\n\nclass SectionNotFound(Exception):\n    pass\n\n\ndef split_pattern(path):  # TODO: Improve skeevy code\n    path = fnmatch.translate(path).split('\\\\/')\n    for i, p in enumerate(path[:-1]):\n        if p:\n            path[i] = p + '\\\\Z(?ms)'\n    return path\n\n# ezyang: This code is pretty skeevy; there is probably a better,\n# more obviously correct way of spelling it. Refactor me...\ndef is_ignored(path, regex):\n    regex = split_pattern(os.path.normcase(regex))\n    path = os.path.normcase(path).split('/')\n\n    regex_pos = path_pos = 0\n    if regex[0] == '':  # leading slash - root dir must match\n        if path[0] != '' or not re.match(regex[1], path[1]):\n            return False\n        regex_pos = path_pos = 2\n\n    if not regex_pos:  # find beginning of regex\n        for i, p in enumerate(path):\n            if re.match(regex[0], p):\n                regex_pos = 1\n                path_pos = i + 1\n                break\n        else:\n            return False\n\n    if len(path[path_pos:]) < len(regex[regex_pos:]):\n        return False\n\n    n = len(regex)\n    for r in regex[regex_pos:]:  # match the rest\n        if regex_pos + 1 == n:  # last item; if empty match anything\n            if re.match(r, ''):\n                return True\n\n        if not re.match(r, path[path_pos]):\n            return False\n        path_pos += 1\n        regex_pos += 1\n\n    return True\n\n\ndef main():\n    Git.git_binary = 'git'  # Windows doesn't like env\n\n    repo, options, args = parse_args()\n\n    if repo.is_dirty() and not options.commit:\n        logging.warning(\"Working copy is dirty; uncommitted changes will NOT be uploaded\")\n\n    base = options.ftp.remotepath\n    logging.info(\"Base directory is %s\", base)\n    try:\n        branch = next(h for h in repo.heads if h.name == options.branch)\n    except StopIteration:\n        raise BranchNotFound\n    commit = branch.commit\n    if options.commit:\n        commit = repo.commit(options.commit)\n    tree = commit.tree\n    if options.ftp.ssl:\n        if hasattr(ftplib, 'FTP_TLS'):  # SSL new in 2.7+\n            ftp = ftplib.FTP_TLS(options.ftp.hostname, options.ftp.username, options.ftp.password)\n            ftp.prot_p()\n            logging.info(\"Using SSL\")\n        else:\n            raise FtpSslNotSupported(\"Python is too old for FTP SSL. Try using Python 2.7 or later.\")\n    else:\n        ftp = ftplib.FTP(options.ftp.hostname, options.ftp.username, options.ftp.password)\n    ftp.cwd(base)\n\n    # Check revision\n    hash = options.revision\n    if not options.force and not hash:\n        hashFile = BytesIO()\n        try:\n            ftp.retrbinary('RETR git-rev.txt', hashFile.write)\n            hash = hashFile.getvalue().strip()\n        except ftplib.error_perm:\n            pass\n\n    # Load ftpignore rules, if any\n    patterns = []\n\n    gitftpignore = os.path.join(repo.working_dir, options.ftp.gitftpignore)\n    if os.path.isfile(gitftpignore):\n        with open(gitftpignore, 'r') as ftpignore:\n            patterns = parse_ftpignore(ftpignore)\n        patterns.append('/' + options.ftp.gitftpignore)\n\n    if not hash:\n        # Diffing against an empty tree will cause a full upload.\n        oldtree = get_empty_tree(repo)\n    else:\n        oldtree = repo.commit(hash).tree\n\n    if oldtree.hexsha == tree.hexsha:\n        logging.info('Nothing to do!')\n    else:\n        upload_diff(repo, oldtree, tree, ftp, [base], patterns)\n\n    ftp.storbinary('STOR git-rev.txt', BytesIO(commit.hexsha.encode('utf-8')))\n    ftp.quit()\n\n\ndef parse_ftpignore(rawPatterns):\n    patterns = []\n    for pat in rawPatterns:\n        pat = pat.rstrip()\n        if not pat or pat.startswith('#'):\n            continue\n        patterns.append(pat)\n    return patterns\n\n\ndef parse_args():\n    usage = 'usage: %prog [OPTIONS] [DIRECTORY]'\n    desc = \"\"\"\\\n           This script uploads files in a Git repository to a\n           website via FTP, but is smart and only uploads file\n           that have changed.\n           \"\"\"\n    parser = optparse.OptionParser(usage, description=textwrap.dedent(desc))\n    parser.add_option('-f', '--force', dest=\"force\", action=\"store_true\", default=False,\n            help=\"force the reupload of all files\")\n    parser.add_option('-q', '--quiet', dest=\"quiet\", action=\"store_true\", default=False,\n            help=\"quiet output\")\n    parser.add_option('-r', '--revision', dest=\"revision\", default=None,\n            help=\"use this revision instead of the server stored one\")\n    parser.add_option('-b', '--branch', dest=\"branch\", default=None,\n            help=\"use this branch instead of the active one\")\n    parser.add_option('-c', '--commit', dest=\"commit\", default=None,\n            help=\"use this commit instead of HEAD\")\n    parser.add_option('-s', '--section', dest=\"section\", default=None,\n            help=\"use this section from ftpdata instead of branch name\")\n    options, args = parser.parse_args()\n    configure_logging(options)\n    if len(args) > 1:\n        parser.error(\"too many arguments\")\n    if args:\n        cwd = args[0]\n    else:\n        cwd = \".\"\n    repo = Repo(cwd)\n\n    if not options.branch:\n        options.branch = repo.active_branch.name\n\n    if not options.section:\n        options.section = options.branch\n\n    get_ftp_creds(repo, options)\n    return repo, options, args\n\n\ndef configure_logging(options):\n    logger = logging.getLogger()\n    if not options.quiet:\n        logger.setLevel(logging.INFO)\n    ch = logging.StreamHandler(sys.stderr)\n    formatter = logging.Formatter(\"%(levelname)s: %(message)s\")\n    ch.setFormatter(formatter)\n    logger.addHandler(ch)\n\n\ndef format_mode(mode):\n    return \"%o\" % (mode & 0o777)\n\n\nclass FtpData():\n    password = None\n    username = None\n    hostname = None\n    remotepath = None\n    ssl = None\n    gitftpignore = None\n\n\ndef get_ftp_creds(repo, options):\n    \"\"\"\n    Retrieves the data to connect to the FTP from .git/ftpdata\n    or interactively.\n\n    ftpdata format example:\n\n        [branch]\n        username=me\n        password=s00perP4zzw0rd\n        hostname=ftp.hostname.com\n        remotepath=/htdocs\n        ssl=yes\n        gitftpignore=.gitftpignore\n\n    Please note that it isn't necessary to have this file,\n    you'll be asked for the data every time you upload something.\n    \"\"\"\n\n    ftpdata = os.path.join(repo.git_dir, \"ftpdata\")\n    options.ftp = FtpData()\n    cfg = ConfigParser.ConfigParser()\n    if os.path.isfile(ftpdata):\n        logging.info(\"Using .git/ftpdata\")\n        cfg.read(ftpdata)\n\n        if (not cfg.has_section(options.section)):\n            if cfg.has_section('ftp'):\n                raise FtpDataOldVersion(\"Please rename the [ftp] section to [branch]. \" +\n                                        \"Take a look at the README for more information\")\n            else:\n                raise SectionNotFound(\"Your .git/ftpdata file does not contain a section \" +\n                                     \"named '%s'\" % options.section)\n\n        # just in case you do not want to store your ftp password.\n        try:\n            options.ftp.password = cfg.get(options.section, 'password')\n        except ConfigParser.NoOptionError:\n            options.ftp.password = getpass.getpass('FTP Password: ')\n\n        options.ftp.username = cfg.get(options.section, 'username')\n        options.ftp.hostname = cfg.get(options.section, 'hostname')\n        options.ftp.remotepath = cfg.get(options.section, 'remotepath')\n        try:\n            options.ftp.ssl = boolish(cfg.get(options.section, 'ssl'))\n        except ConfigParser.NoOptionError:\n            options.ftp.ssl = False\n\n        try:\n            options.ftp.gitftpignore = cfg.get(options.section, 'gitftpignore')\n        except ConfigParser.NoOptionError:\n            options.ftp.gitftpignore = '.gitftpignore'\n    else:\n        print(\"Please configure settings for branch '%s'\" % options.section)\n        options.ftp.username = raw_input('FTP Username: ')\n        options.ftp.password = getpass.getpass('FTP Password: ')\n        options.ftp.hostname = raw_input('FTP Hostname: ')\n        options.ftp.remotepath = raw_input('Remote Path: ')\n        if hasattr(ftplib, 'FTP_TLS'):\n            options.ftp.ssl = ask_ok('Use SSL? ')\n        else:\n            logging.warning(\"SSL not supported, defaulting to no\")\n\n        # set default branch\n        if ask_ok(\"Should I write ftp details to .git/ftpdata? \"):\n            cfg.add_section(options.section)\n            cfg.set(options.section, 'username', options.ftp.username)\n            cfg.set(options.section, 'password', options.ftp.password)\n            cfg.set(options.section, 'hostname', options.ftp.hostname)\n            cfg.set(options.section, 'remotepath', options.ftp.remotepath)\n            cfg.set(options.section, 'ssl', options.ftp.ssl)\n            f = open(ftpdata, 'w')\n            cfg.write(f)\n\n\ndef get_empty_tree(repo):\n    return repo.tree(repo.git.hash_object('-w', '-t', 'tree', os.devnull))\n\n\ndef upload_diff(repo, oldtree, tree, ftp, base, ignored):\n    \"\"\"\n    Upload  and/or delete items according to a Git diff between two trees.\n\n    upload_diff requires, that the ftp working directory is set to the base\n    of the current repository before it is called.\n\n    Keyword arguments:\n    repo    -- The git.Repo to upload objects from\n    oldtree -- The old tree to diff against. An empty tree will cause a full\n               upload of the new tree.\n    tree    -- The new tree. An empty tree will cause a full removal of all\n               objects of the old tree.\n    ftp     -- The active ftplib.FTP object to upload contents to\n    base    -- The list of base directory and submodule paths to upload contents\n               to in ftp.\n               For example, base = ['www', 'www']. base must exist and must not\n               have a trailing slash.\n    ignored -- The list of patterns explicitly ignored by gitftpignore.\n\n    \"\"\"\n    # -z is used so we don't have to deal with quotes in path matching\n    diff = repo.git.diff(\"--name-status\", \"--no-renames\", \"-z\", oldtree.hexsha, tree.hexsha)\n    diff = iter(diff.split(\"\\0\"))\n    for line in diff:\n        if not line:\n            continue\n        status, file = line, next(diff)\n        assert status in ['A', 'D', 'M']\n\n        filepath = posixpath.join(*(['/'] + base[1:] + [file]))\n        if is_ignored_path(filepath, ignored):\n            logging.info('Skipped ' + filepath)\n            continue\n\n        if status == \"D\":\n            try:\n                ftp.delete(file)\n                logging.info('Deleted ' + file)\n            except ftplib.error_perm:\n                logging.warning('Failed to delete ' + file)\n\n            # Now let's see if we need to remove some subdirectories\n            def generate_parent_dirs(x):\n                # invariant: x is a filename\n                while '/' in x:\n                    x = posixpath.dirname(x)\n                    yield x\n            for dir in generate_parent_dirs(file):\n                try:\n                    # unfortunately, dir in tree doesn't work for subdirs\n                    tree[dir]\n                except KeyError:\n                    try:\n                        ftp.rmd(dir)\n                        logging.debug('Cleaned away ' + dir)\n                    except ftplib.error_perm:\n                        logging.info('Did not clean away ' + dir)\n                        break\n        else:\n            node = tree[file]\n\n            if status == \"A\":\n                # try building up the parent directory\n                subtree = tree\n                if isinstance(node, Blob):\n                    directories = file.split(\"/\")[:-1]\n                else:\n                    # for submodules also add the directory itself\n                    assert isinstance(node, Submodule)\n                    directories = file.split(\"/\")\n                for c in directories:\n                    subtree = subtree / c\n                    try:\n                        ftp.mkd(subtree.path)\n                    except ftplib.error_perm:\n                        pass\n\n            if isinstance(node, Blob):\n                upload_blob(node, ftp)\n            else:\n                module = node.module()\n                module_tree = module.commit(node.hexsha).tree\n                if status == \"A\":\n                    module_oldtree = get_empty_tree(module)\n                else:\n                    oldnode = oldtree[file]\n                    assert isinstance(oldnode, Submodule)  # TODO: What if not?\n                    module_oldtree = module.commit(oldnode.hexsha).tree\n                module_base = base + [node.path]\n                logging.info('Entering submodule %s', node.path)\n                ftp.cwd(posixpath.join(*module_base))\n                upload_diff(module, module_oldtree, module_tree, ftp, module_base, ignored)\n                logging.info('Leaving submodule %s', node.path)\n                ftp.cwd(posixpath.join(*base))\n\n\ndef is_ignored_path(path, patterns, quiet=False):\n    \"\"\"Returns true if a filepath is ignored by gitftpignore.\"\"\"\n    if is_special_file(path):\n        return True\n    for pat in patterns:\n        if is_ignored(path, pat):\n            return True\n    return False\n\n\ndef is_special_file(name):\n    \"\"\"Returns true if a file is some special Git metadata and not content.\"\"\"\n    return posixpath.basename(name) in ['.gitignore', '.gitattributes', '.gitmodules']\n\n\ndef upload_blob(blob, ftp, quiet=False):\n    \"\"\"\n    Uploads a blob.  Pre-condition on ftp is that our current working\n    directory is the root directory of the repository being uploaded\n    (that means DON'T use ftp.cwd; we'll use full paths appropriately).\n    \"\"\"\n    if not quiet:\n        logging.info('Uploading ' + blob.path)\n    try:\n        ftp.delete(blob.path)\n    except ftplib.error_perm:\n        pass\n    ftp.storbinary('STOR ' + blob.path, blob.data_stream)\n    try:\n        ftp.voidcmd('SITE CHMOD ' + format_mode(blob.mode) + ' ' + blob.path)\n    except ftplib.error_perm:\n        # Ignore Windows chmod errors\n        logging.warning('Failed to chmod ' + blob.path)\n        pass\n\n\ndef boolish(s):\n    if s in ('1', 'true', 'y', 'ye', 'yes', 'on'):\n        return True\n    if s in ('0', 'false', 'n', 'no', 'off'):\n        return False\n    return None\n\n\ndef ask_ok(prompt, retries=4, complaint='Yes or no, please!'):\n    while True:\n        ok = raw_input(prompt).lower()\n        r = boolish(ok)\n        if r is not None:\n            return r\n        retries = retries - 1\n        if retries < 0:\n            raise IOError('Wrong user input.')\n        print(complaint)\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "post-receive",
    "content": "#!/bin/bash\n# You may install this post-receive hook in your remote git repository\n# to have automatic file upload when pushing to the repository.\nwhile read OLD_COMMIT NEW_COMMIT REFNAME; do\n    BRANCH=${REFNAME#refs/heads/}\n\n    if [[ `grep \"^\\[$BRANCH\\]$\" ftpdata` ]]; then\n        echo \"Uploading $BRANCH...\"\n        $(dirname $(readlink -f \"$0\"))/git-ftp.py -b \"$BRANCH\" -c \"$NEW_COMMIT\"\t|| exit $?\n    fi\ndone\ntrue\n"
  }
]