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 | \-c) ] [(\-\-branch | \-b) ] [(\-\-section | \-s)
] .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 , \-\-revision= .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= .RS 4 Upload this commit instead of HEAD or the tip of the selected branch\&. .RE .PP \-b , \-\-branch= .RS 4 Use this branch instead of the active one\&. .RE .PP \-s
, \-\-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 , Mauro Lizaur and Niklas Fiekas .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 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 , Mauro Lizaur and Niklas Fiekas 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