1#! /usr/bin/env python
2
3import errno
4import optparse as op
5import os
6import subprocess as sp
7import sys
8import time
9from dateutil import tz
10from datetime import datetime
11
12try:
13    from shlex import quote
14except ImportError:
15    from pipes import quote
16
17__all__ = ['ghp_import']
18__version__ = "2.0.2"
19__usage__ = "%prog [OPTIONS] DIRECTORY"
20
21
22class GhpError(Exception):
23    def __init__(self, message):
24        self.message = message
25
26
27if sys.version_info[0] == 3:
28    def enc(text):
29        if isinstance(text, bytes):
30            return text
31        return text.encode()
32
33    def dec(text):
34        if isinstance(text, bytes):
35            return text.decode('utf-8')
36        return text
37
38    def write(pipe, data):
39        try:
40            pipe.stdin.write(data)
41        except IOError as e:
42            if e.errno != errno.EPIPE:
43                raise
44else:
45    def enc(text):
46        if isinstance(text, unicode):  # noqa F821
47            return text.encode('utf-8')
48        return text
49
50    def dec(text):
51        if isinstance(text, unicode):  # noqa F821
52            return text
53        return text.decode('utf-8')
54
55    def write(pipe, data):
56        pipe.stdin.write(data)
57
58
59class Git(object):
60    def __init__(self, use_shell=False):
61        self.use_shell = use_shell
62
63        self.cmd = None
64        self.pipe = None
65        self.stderr = None
66        self.stdout = None
67
68    def check_repo(self):
69        if self.call('rev-parse') != 0:
70            error = self.stderr
71            if not error:
72                error = "Unknown Git error"
73            error = dec(error)
74            if error.startswith("fatal: "):
75                error = error[len("fatal: "):]
76            raise GhpError(error)
77
78    def try_rebase(self, remote, branch, no_history=False):
79        rc = self.call('rev-list', '--max-count=1', '%s/%s' % (remote, branch))
80        if rc != 0:
81            return True
82        rev = dec(self.stdout.strip())
83        if no_history:
84            rc = self.call('update-ref', '-d', 'refs/heads/%s' % branch)
85        else:
86            rc = self.call('update-ref', 'refs/heads/%s' % branch, rev)
87        if rc != 0:
88            return False
89        return True
90
91    def get_config(self, key):
92        self.call('config', key)
93        return self.stdout.strip()
94
95    def get_prev_commit(self, branch):
96        rc = self.call('rev-list', '--max-count=1', branch, '--')
97        if rc != 0:
98            return None
99        return dec(self.stdout).strip()
100
101    def open(self, *args, **kwargs):
102        if self.use_shell:
103            self.cmd = 'git ' + ' '.join(map(quote, args))
104        else:
105            self.cmd = ['git'] + list(args)
106        if sys.version_info >= (3, 2, 0):
107            kwargs['universal_newlines'] = False
108        for k in 'stdin stdout stderr'.split():
109            kwargs.setdefault(k, sp.PIPE)
110        kwargs['shell'] = self.use_shell
111        self.pipe = sp.Popen(self.cmd, **kwargs)
112        return self.pipe
113
114    def call(self, *args, **kwargs):
115        self.open(*args, **kwargs)
116        (self.stdout, self.stderr) = self.pipe.communicate()
117        return self.pipe.wait()
118
119    def check_call(self, *args, **kwargs):
120        kwargs["shell"] = self.use_shell
121        sp.check_call(['git'] + list(args), **kwargs)
122
123
124def mk_when(timestamp=None):
125    if timestamp is None:
126        timestamp = int(time.time())
127    currtz = datetime.now(tz.tzlocal()).strftime('%z')
128    return "%s %s" % (timestamp, currtz)
129
130
131def start_commit(pipe, git, branch, message, prefix=None):
132    uname = os.getenv('GIT_COMMITTER_NAME', dec(git.get_config('user.name')))
133    email = os.getenv('GIT_COMMITTER_EMAIL', dec(git.get_config('user.email')))
134    when = os.getenv('GIT_COMMITTER_DATE', mk_when())
135    write(pipe, enc('commit refs/heads/%s\n' % branch))
136    write(pipe, enc('committer %s <%s> %s\n' % (uname, email, when)))
137    write(pipe, enc('data %d\n%s\n' % (len(enc(message)), message)))
138    head = git.get_prev_commit(branch)
139    if head:
140        write(pipe, enc('from %s\n' % head))
141    if prefix:
142        write(pipe, enc('D %s\n' % prefix))
143    else:
144        write(pipe, enc('deleteall\n'))
145
146
147def add_file(pipe, srcpath, tgtpath):
148    with open(srcpath, "rb") as handle:
149        if os.access(srcpath, os.X_OK):
150            write(pipe, enc('M 100755 inline %s\n' % tgtpath))
151        else:
152            write(pipe, enc('M 100644 inline %s\n' % tgtpath))
153        data = handle.read()
154        write(pipe, enc('data %d\n' % len(data)))
155        write(pipe, enc(data))
156        write(pipe, enc('\n'))
157
158
159def add_nojekyll(pipe):
160    write(pipe, enc('M 100644 inline .nojekyll\n'))
161    write(pipe, enc('data 0\n'))
162    write(pipe, enc('\n'))
163
164
165def add_cname(pipe, cname):
166    write(pipe, enc('M 100644 inline CNAME\n'))
167    write(pipe, enc('data %d\n%s\n' % (len(enc(cname)), cname)))
168
169
170def gitpath(fname):
171    norm = os.path.normpath(fname)
172    return "/".join(norm.split(os.path.sep))
173
174
175def run_import(git, srcdir, **opts):
176    srcdir = dec(srcdir)
177    pipe = git.open('fast-import', '--date-format=raw', '--quiet',
178                    stdin=sp.PIPE, stdout=None, stderr=None)
179    start_commit(pipe, git, opts['branch'], opts['mesg'], opts['prefix'])
180    for path, _, fnames in os.walk(srcdir, followlinks=opts['followlinks']):
181        for fn in fnames:
182            fpath = os.path.join(path, fn)
183            gpath = gitpath(os.path.relpath(fpath, start=srcdir))
184            if opts['prefix']:
185                gpath = os.path.join(opts['prefix'], gpath)
186            add_file(pipe, fpath, gpath)
187    if opts['nojekyll']:
188        add_nojekyll(pipe)
189    if opts['cname'] is not None:
190        add_cname(pipe, opts['cname'])
191    write(pipe, enc('\n'))
192    pipe.stdin.close()
193    if pipe.wait() != 0:
194        sys.stdout.write(enc("Failed to process commit.\n"))
195
196
197def options():
198    return [
199        op.make_option(
200            '-n', '--no-jekyll', dest='nojekyll', default=False,
201            action="store_true",
202            help='Include a .nojekyll file in the branch.'),
203        op.make_option(
204            '-c', '--cname', dest='cname', default=None,
205            help='Write a CNAME file with the given CNAME.'),
206        op.make_option(
207            '-m', '--message', dest='mesg',
208            default='Update documentation',
209            help='The commit message to use on the target branch.'),
210        op.make_option(
211            '-p', '--push', dest='push', default=False,
212            action='store_true',
213            help='Push the branch to origin/{branch} after committing.'),
214        op.make_option(
215            '-x', '--prefix', dest='prefix', default=None,
216            help='The prefix to add to each file that gets pushed to the '
217                 'remote. Only files below this prefix will be cleared '
218                 'out. [%default]'),
219        op.make_option(
220            '-f', '--force', dest='force',
221            default=False, action='store_true',
222            help='Force the push to the repository.'),
223        op.make_option(
224            '-o', '--no-history', dest='no_history',
225            default=False, action='store_true',
226            help='Force new commit without parent history.'),
227        op.make_option(
228            '-r', '--remote', dest='remote', default='origin',
229            help='The name of the remote to push to. [%default]'),
230        op.make_option(
231            '-b', '--branch', dest='branch', default='gh-pages',
232            help='Name of the branch to write to. [%default]'),
233        op.make_option(
234            '-s', '--shell', dest='use_shell', default=False,
235            action='store_true',
236            help='Use the shell when invoking Git. [%default]'),
237        op.make_option(
238            '-l', '--follow-links', dest='followlinks',
239            default=False, action='store_true',
240            help='Follow symlinks when adding files. [%default]')
241    ]
242
243
244def ghp_import(srcdir, **kwargs):
245    if not os.path.isdir(srcdir):
246        raise GhpError("Not a directory: %s" % srcdir)
247
248    opts = {opt.dest: opt.default for opt in options()}
249    opts.update(kwargs)
250
251    git = Git(use_shell=opts['use_shell'])
252    git.check_repo()
253
254    if not git.try_rebase(opts['remote'], opts['branch'], opts['no_history']):
255        raise GhpError("Failed to rebase %s branch." % opts['branch'])
256
257    run_import(git, srcdir, **opts)
258
259    if opts['push']:
260        if opts['force'] or opts['no_history']:
261            git.check_call('push', opts['remote'], opts['branch'], '--force')
262        else:
263            git.check_call('push', opts['remote'], opts['branch'])
264
265
266def main():
267    parser = op.OptionParser(usage=__usage__, option_list=options(),
268                             version=__version__)
269    opts, args = parser.parse_args()
270
271    if len(args) == 0:
272        parser.error("No import directory specified.")
273
274    if len(args) > 1:
275        parser.error("Unknown arguments specified: %s" % ', '.join(args[1:]))
276
277    try:
278        ghp_import(args[0], **opts.__dict__)
279    except GhpError as e:
280        parser.error(e.message)
281
282
283if __name__ == '__main__':
284    main()
285