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