blob: a2cc4f76cbad0c07cabe428fd2a08a17c6288b62 [file] [log] [blame]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001#!/usr/bin/python
2# git-cl -- a git-command for integrating reviews on Rietveld
3# Copyright (C) 2008 Evan Martin <martine@danga.com>
4
5import errno
6import logging
7import optparse
8import os
9import re
10import subprocess
11import sys
12import tempfile
13import textwrap
14import upload
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000015import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import urllib2
17
18try:
19 import readline
20except ImportError:
21 pass
22
23try:
24 # Add the parent directory in case it's a depot_tools checkout.
25 depot_tools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26 sys.path.append(depot_tools_path)
27 import breakpad
28except ImportError:
29 pass
30
31DEFAULT_SERVER = 'http://codereview.appspot.com'
32PREDCOMMIT_HOOK = '.git/hooks/pre-cl-dcommit'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000033POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034PREUPLOAD_HOOK = '.git/hooks/pre-cl-upload'
35DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
36
37def DieWithError(message):
38 print >>sys.stderr, message
39 sys.exit(1)
40
41
42def Popen(cmd, **kwargs):
43 """Wrapper for subprocess.Popen() that logs and watch for cygwin issues"""
44 logging.info('Popen: ' + ' '.join(cmd))
45 try:
46 return subprocess.Popen(cmd, **kwargs)
47 except OSError, e:
48 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
49 DieWithError(
50 'Visit '
51 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
52 'learn how to fix this error; you need to rebase your cygwin dlls')
53 raise
54
55
56def RunCommand(cmd, error_ok=False, error_message=None,
57 redirect_stdout=True, swallow_stderr=False):
58 if redirect_stdout:
59 stdout = subprocess.PIPE
60 else:
61 stdout = None
62 if swallow_stderr:
63 stderr = subprocess.PIPE
64 else:
65 stderr = None
66 proc = Popen(cmd, stdout=stdout, stderr=stderr)
67 output = proc.communicate()[0]
68 if not error_ok and proc.returncode != 0:
69 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) +
70 (error_message or output or ''))
71 return output
72
73
74def RunGit(args, **kwargs):
75 cmd = ['git'] + args
76 return RunCommand(cmd, **kwargs)
77
78
79def RunGitWithCode(args):
80 proc = Popen(['git'] + args, stdout=subprocess.PIPE)
81 output = proc.communicate()[0]
82 return proc.returncode, output
83
84
85def usage(more):
86 def hook(fn):
87 fn.usage_more = more
88 return fn
89 return hook
90
91
92def FixUrl(server):
93 """Fix a server url to defaults protocol to http:// if none is specified."""
94 if not server:
95 return server
96 if not re.match(r'[a-z]+\://.*', server):
97 return 'http://' + server
98 return server
99
100
101class Settings(object):
102 def __init__(self):
103 self.default_server = None
104 self.cc = None
105 self.root = None
106 self.is_git_svn = None
107 self.svn_branch = None
108 self.tree_status_url = None
109 self.viewvc_url = None
110 self.updated = False
111
112 def LazyUpdateIfNeeded(self):
113 """Updates the settings from a codereview.settings file, if available."""
114 if not self.updated:
115 cr_settings_file = FindCodereviewSettingsFile()
116 if cr_settings_file:
117 LoadCodereviewSettingsFromFile(cr_settings_file)
118 self.updated = True
119
120 def GetDefaultServerUrl(self, error_ok=False):
121 if not self.default_server:
122 self.LazyUpdateIfNeeded()
123 self.default_server = FixUrl(self._GetConfig('rietveld.server',
124 error_ok=True))
125 if error_ok:
126 return self.default_server
127 if not self.default_server:
128 error_message = ('Could not find settings file. You must configure '
129 'your review setup by running "git cl config".')
130 self.default_server = FixUrl(self._GetConfig(
131 'rietveld.server', error_message=error_message))
132 return self.default_server
133
134 def GetCCList(self):
135 """Return the users cc'd on this CL.
136
137 Return is a string suitable for passing to gcl with the --cc flag.
138 """
139 if self.cc is None:
140 self.cc = self._GetConfig('rietveld.cc', error_ok=True)
141 more_cc = self._GetConfig('rietveld.extracc', error_ok=True)
142 if more_cc is not None:
143 self.cc += ',' + more_cc
144 return self.cc
145
146 def GetRoot(self):
147 if not self.root:
148 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
149 return self.root
150
151 def GetIsGitSvn(self):
152 """Return true if this repo looks like it's using git-svn."""
153 if self.is_git_svn is None:
154 # If you have any "svn-remote.*" config keys, we think you're using svn.
155 self.is_git_svn = RunGitWithCode(
156 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
157 return self.is_git_svn
158
159 def GetSVNBranch(self):
160 if self.svn_branch is None:
161 if not self.GetIsGitSvn():
162 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
163
164 # Try to figure out which remote branch we're based on.
165 # Strategy:
166 # 1) find all git-svn branches and note their svn URLs.
167 # 2) iterate through our branch history and match up the URLs.
168
169 # regexp matching the git-svn line that contains the URL.
170 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
171
172 # Get the refname and svn url for all refs/remotes/*.
173 remotes = RunGit(['for-each-ref', '--format=%(refname)',
174 'refs/remotes']).splitlines()
175 svn_refs = {}
176 for ref in remotes:
177 match = git_svn_re.search(RunGit(['cat-file', '-p', ref]))
178 # Prefer origin/HEAD over all others.
179 if match and (match.group(1) not in svn_refs or
180 ref == "refs/remotes/origin/HEAD"):
181 svn_refs[match.group(1)] = ref
182
183 if len(svn_refs) == 1:
184 # Only one svn branch exists -- seems like a good candidate.
185 self.svn_branch = svn_refs.values()[0]
186 elif len(svn_refs) > 1:
187 # We have more than one remote branch available. We don't
188 # want to go through all of history, so read a line from the
189 # pipe at a time.
190 # The -100 is an arbitrary limit so we don't search forever.
191 cmd = ['git', 'log', '-100', '--pretty=medium']
192 proc = Popen(cmd, stdout=subprocess.PIPE)
193 for line in proc.stdout:
194 match = git_svn_re.match(line)
195 if match:
196 url = match.group(1)
197 if url in svn_refs:
198 self.svn_branch = svn_refs[url]
199 proc.stdout.close() # Cut pipe.
200 break
201
202 if not self.svn_branch:
203 DieWithError('Can\'t guess svn branch -- try specifying it on the '
204 'command line')
205
206 return self.svn_branch
207
208 def GetTreeStatusUrl(self, error_ok=False):
209 if not self.tree_status_url:
210 error_message = ('You must configure your tree status URL by running '
211 '"git cl config".')
212 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
213 error_ok=error_ok,
214 error_message=error_message)
215 return self.tree_status_url
216
217 def GetViewVCUrl(self):
218 if not self.viewvc_url:
219 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
220 return self.viewvc_url
221
222 def _GetConfig(self, param, **kwargs):
223 self.LazyUpdateIfNeeded()
224 return RunGit(['config', param], **kwargs).strip()
225
226
227settings = Settings()
228
229
230did_migrate_check = False
231def CheckForMigration():
232 """Migrate from the old issue format, if found.
233
234 We used to store the branch<->issue mapping in a file in .git, but it's
235 better to store it in the .git/config, since deleting a branch deletes that
236 branch's entry there.
237 """
238
239 # Don't run more than once.
240 global did_migrate_check
241 if did_migrate_check:
242 return
243
244 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
245 storepath = os.path.join(gitdir, 'cl-mapping')
246 if os.path.exists(storepath):
247 print "old-style git-cl mapping file (%s) found; migrating." % storepath
248 store = open(storepath, 'r')
249 for line in store:
250 branch, issue = line.strip().split()
251 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
252 issue])
253 store.close()
254 os.remove(storepath)
255 did_migrate_check = True
256
257
258def ShortBranchName(branch):
259 """Convert a name like 'refs/heads/foo' to just 'foo'."""
260 return branch.replace('refs/heads/', '')
261
262
263class Changelist(object):
264 def __init__(self, branchref=None):
265 # Poke settings so we get the "configure your server" message if necessary.
266 settings.GetDefaultServerUrl()
267 self.branchref = branchref
268 if self.branchref:
269 self.branch = ShortBranchName(self.branchref)
270 else:
271 self.branch = None
272 self.rietveld_server = None
273 self.upstream_branch = None
274 self.has_issue = False
275 self.issue = None
276 self.has_description = False
277 self.description = None
278 self.has_patchset = False
279 self.patchset = None
280
281 def GetBranch(self):
282 """Returns the short branch name, e.g. 'master'."""
283 if not self.branch:
284 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
285 self.branch = ShortBranchName(self.branchref)
286 return self.branch
287
288 def GetBranchRef(self):
289 """Returns the full branch name, e.g. 'refs/heads/master'."""
290 self.GetBranch() # Poke the lazy loader.
291 return self.branchref
292
293 def FetchUpstreamTuple(self):
294 """Returns a tuple containg remote and remote ref,
295 e.g. 'origin', 'refs/heads/master'
296 """
297 remote = '.'
298 branch = self.GetBranch()
299 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
300 error_ok=True).strip()
301 if upstream_branch:
302 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
303 else:
304 # Fall back on trying a git-svn upstream branch.
305 if settings.GetIsGitSvn():
306 upstream_branch = settings.GetSVNBranch()
307 else:
308 # Else, try to guess the origin remote.
309 remote_branches = RunGit(['branch', '-r']).split()
310 if 'origin/master' in remote_branches:
311 # Fall back on origin/master if it exits.
312 remote = 'origin'
313 upstream_branch = 'refs/heads/master'
314 elif 'origin/trunk' in remote_branches:
315 # Fall back on origin/trunk if it exists. Generally a shared
316 # git-svn clone
317 remote = 'origin'
318 upstream_branch = 'refs/heads/trunk'
319 else:
320 DieWithError("""Unable to determine default branch to diff against.
321Either pass complete "git diff"-style arguments, like
322 git cl upload origin/master
323or verify this branch is set up to track another (via the --track argument to
324"git checkout -b ...").""")
325
326 return remote, upstream_branch
327
328 def GetUpstreamBranch(self):
329 if self.upstream_branch is None:
330 remote, upstream_branch = self.FetchUpstreamTuple()
331 if remote is not '.':
332 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
333 self.upstream_branch = upstream_branch
334 return self.upstream_branch
335
336 def GetRemoteUrl(self):
337 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
338
339 Returns None if there is no remote.
340 """
341 remote = self.FetchUpstreamTuple()[0]
342 if remote == '.':
343 return None
344 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
345
346 def GetIssue(self):
347 if not self.has_issue:
348 CheckForMigration()
349 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
350 if issue:
351 self.issue = issue
352 self.rietveld_server = FixUrl(RunGit(
353 ['config', self._RietveldServer()], error_ok=True).strip())
354 else:
355 self.issue = None
356 if not self.rietveld_server:
357 self.rietveld_server = settings.GetDefaultServerUrl()
358 self.has_issue = True
359 return self.issue
360
361 def GetRietveldServer(self):
362 self.GetIssue()
363 return self.rietveld_server
364
365 def GetIssueURL(self):
366 """Get the URL for a particular issue."""
367 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
368
369 def GetDescription(self, pretty=False):
370 if not self.has_description:
371 if self.GetIssue():
372 path = '/' + self.GetIssue() + '/description'
373 rpc_server = self._RpcServer()
374 self.description = rpc_server.Send(path).strip()
375 self.has_description = True
376 if pretty:
377 wrapper = textwrap.TextWrapper()
378 wrapper.initial_indent = wrapper.subsequent_indent = ' '
379 return wrapper.fill(self.description)
380 return self.description
381
382 def GetPatchset(self):
383 if not self.has_patchset:
384 patchset = RunGit(['config', self._PatchsetSetting()],
385 error_ok=True).strip()
386 if patchset:
387 self.patchset = patchset
388 else:
389 self.patchset = None
390 self.has_patchset = True
391 return self.patchset
392
393 def SetPatchset(self, patchset):
394 """Set this branch's patchset. If patchset=0, clears the patchset."""
395 if patchset:
396 RunGit(['config', self._PatchsetSetting(), str(patchset)])
397 else:
398 RunGit(['config', '--unset', self._PatchsetSetting()],
399 swallow_stderr=True, error_ok=True)
400 self.has_patchset = False
401
402 def SetIssue(self, issue):
403 """Set this branch's issue. If issue=0, clears the issue."""
404 if issue:
405 RunGit(['config', self._IssueSetting(), str(issue)])
406 if self.rietveld_server:
407 RunGit(['config', self._RietveldServer(), self.rietveld_server])
408 else:
409 RunGit(['config', '--unset', self._IssueSetting()])
410 self.SetPatchset(0)
411 self.has_issue = False
412
413 def CloseIssue(self):
414 rpc_server = self._RpcServer()
415 # Newer versions of Rietveld require us to pass an XSRF token to POST, so
416 # we fetch it from the server. (The version used by Chromium has been
417 # modified so the token isn't required when closing an issue.)
418 xsrf_token = rpc_server.Send('/xsrf_token',
419 extra_headers={'X-Requesting-XSRF-Token': '1'})
420
421 # You cannot close an issue with a GET.
422 # We pass an empty string for the data so it is a POST rather than a GET.
423 data = [("description", self.description),
424 ("xsrf_token", xsrf_token)]
425 ctype, body = upload.EncodeMultipartFormData(data, [])
426 rpc_server.Send('/' + self.GetIssue() + '/close', body, ctype)
427
428 def _RpcServer(self):
429 """Returns an upload.RpcServer() to access this review's rietveld instance.
430 """
431 server = self.GetRietveldServer()
432 return upload.GetRpcServer(server, save_cookies=True)
433
434 def _IssueSetting(self):
435 """Return the git setting that stores this change's issue."""
436 return 'branch.%s.rietveldissue' % self.GetBranch()
437
438 def _PatchsetSetting(self):
439 """Return the git setting that stores this change's most recent patchset."""
440 return 'branch.%s.rietveldpatchset' % self.GetBranch()
441
442 def _RietveldServer(self):
443 """Returns the git setting that stores this change's rietveld server."""
444 return 'branch.%s.rietveldserver' % self.GetBranch()
445
446
447def GetCodereviewSettingsInteractively():
448 """Prompt the user for settings."""
449 server = settings.GetDefaultServerUrl(error_ok=True)
450 prompt = 'Rietveld server (host[:port])'
451 prompt += ' [%s]' % (server or DEFAULT_SERVER)
452 newserver = raw_input(prompt + ': ')
453 if not server and not newserver:
454 newserver = DEFAULT_SERVER
455 if newserver and newserver != server:
456 RunGit(['config', 'rietveld.server', newserver])
457
458 def SetProperty(initial, caption, name):
459 prompt = caption
460 if initial:
461 prompt += ' ("x" to clear) [%s]' % initial
462 new_val = raw_input(prompt + ': ')
463 if new_val == 'x':
464 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
465 elif new_val and new_val != initial:
466 RunGit(['config', 'rietveld.' + name, new_val])
467
468 SetProperty(settings.GetCCList(), 'CC list', 'cc')
469 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
470 'tree-status-url')
471 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
472
473 # TODO: configure a default branch to diff against, rather than this
474 # svn-based hackery.
475
476
477def FindCodereviewSettingsFile(filename='codereview.settings'):
478 """Finds the given file starting in the cwd and going up.
479
480 Only looks up to the top of the repository unless an
481 'inherit-review-settings-ok' file exists in the root of the repository.
482 """
483 inherit_ok_file = 'inherit-review-settings-ok'
484 cwd = os.getcwd()
485 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
486 if os.path.isfile(os.path.join(root, inherit_ok_file)):
487 root = '/'
488 while True:
489 if filename in os.listdir(cwd):
490 if os.path.isfile(os.path.join(cwd, filename)):
491 return open(os.path.join(cwd, filename))
492 if cwd == root:
493 break
494 cwd = os.path.dirname(cwd)
495
496
497def LoadCodereviewSettingsFromFile(fileobj):
498 """Parse a codereview.settings file and updates hooks."""
499 def DownloadToFile(url, filename):
500 filename = os.path.join(settings.GetRoot(), filename)
501 contents = urllib2.urlopen(url).read()
502 fileobj = open(filename, 'w')
503 fileobj.write(contents)
504 fileobj.close()
505 os.chmod(filename, 0755)
506 return 0
507
508 keyvals = {}
509 for line in fileobj.read().splitlines():
510 if not line or line.startswith("#"):
511 continue
512 k, v = line.split(": ", 1)
513 keyvals[k] = v
514
515 def GetProperty(name):
516 return keyvals.get(name)
517
518 def SetProperty(name, setting, unset_error_ok=False):
519 fullname = 'rietveld.' + name
520 if setting in keyvals:
521 RunGit(['config', fullname, keyvals[setting]])
522 else:
523 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
524
525 SetProperty('server', 'CODE_REVIEW_SERVER')
526 # Only server setting is required. Other settings can be absent.
527 # In that case, we ignore errors raised during option deletion attempt.
528 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
529 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
530 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
531
532 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
533 #should be of the form
534 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
535 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
536 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
537 keyvals['ORIGIN_URL_CONFIG']])
538
539 # Update the hooks if the local hook files aren't present already.
540 if GetProperty('GITCL_PREUPLOAD') and not os.path.isfile(PREUPLOAD_HOOK):
541 DownloadToFile(GetProperty('GITCL_PREUPLOAD'), PREUPLOAD_HOOK)
542 if GetProperty('GITCL_PREDCOMMIT') and not os.path.isfile(PREDCOMMIT_HOOK):
543 DownloadToFile(GetProperty('GITCL_PREDCOMMIT'), PREDCOMMIT_HOOK)
544 return 0
545
546
547@usage('[repo root containing codereview.settings]')
548def CMDconfig(parser, args):
549 """edit configuration for this tree"""
550
551 (options, args) = parser.parse_args(args)
552 if len(args) == 0:
553 GetCodereviewSettingsInteractively()
554 return 0
555
556 url = args[0]
557 if not url.endswith('codereview.settings'):
558 url = os.path.join(url, 'codereview.settings')
559
560 # Load code review settings and download hooks (if available).
561 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
562 return 0
563
564
565def CMDstatus(parser, args):
566 """show status of changelists"""
567 parser.add_option('--field',
568 help='print only specific field (desc|id|patch|url)')
569 (options, args) = parser.parse_args(args)
570
571 # TODO: maybe make show_branches a flag if necessary.
572 show_branches = not options.field
573
574 if show_branches:
575 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
576 if branches:
577 print 'Branches associated with reviews:'
578 for branch in sorted(branches.splitlines()):
579 cl = Changelist(branchref=branch)
580 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
581
582 cl = Changelist()
583 if options.field:
584 if options.field.startswith('desc'):
585 print cl.GetDescription()
586 elif options.field == 'id':
587 issueid = cl.GetIssue()
588 if issueid:
589 print issueid
590 elif options.field == 'patch':
591 patchset = cl.GetPatchset()
592 if patchset:
593 print patchset
594 elif options.field == 'url':
595 url = cl.GetIssueURL()
596 if url:
597 print url
598 else:
599 print
600 print 'Current branch:',
601 if not cl.GetIssue():
602 print 'no issue assigned.'
603 return 0
604 print cl.GetBranch()
605 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
606 print 'Issue description:'
607 print cl.GetDescription(pretty=True)
608 return 0
609
610
611@usage('[issue_number]')
612def CMDissue(parser, args):
613 """Set or display the current code review issue number.
614
615 Pass issue number 0 to clear the current issue.
616"""
617 (options, args) = parser.parse_args(args)
618
619 cl = Changelist()
620 if len(args) > 0:
621 try:
622 issue = int(args[0])
623 except ValueError:
624 DieWithError('Pass a number to set the issue or none to list it.\n'
625 'Maybe you want to run git cl status?')
626 cl.SetIssue(issue)
627 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
628 return 0
629
630
631def CreateDescriptionFromLog(args):
632 """Pulls out the commit log to use as a base for the CL description."""
633 log_args = []
634 if len(args) == 1 and not args[0].endswith('.'):
635 log_args = [args[0] + '..']
636 elif len(args) == 1 and args[0].endswith('...'):
637 log_args = [args[0][:-1]]
638 elif len(args) == 2:
639 log_args = [args[0] + '..' + args[1]]
640 else:
641 log_args = args[:] # Hope for the best!
642 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
643
644
645def UserEditedLog(starting_text):
646 """Given some starting text, let the user edit it and return the result."""
647 editor = os.getenv('EDITOR', 'vi')
648
649 (file_handle, filename) = tempfile.mkstemp()
650 fileobj = os.fdopen(file_handle, 'w')
651 fileobj.write(starting_text)
652 fileobj.close()
653
654 ret = subprocess.call(editor + ' ' + filename, shell=True)
655 if ret != 0:
656 os.remove(filename)
657 return
658
659 fileobj = open(filename)
660 text = fileobj.read()
661 fileobj.close()
662 os.remove(filename)
663 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
664 return stripcomment_re.sub('', text).strip()
665
666
667def RunHook(hook, upstream_branch, error_ok=False):
668 """Run a given hook if it exists. By default, we fail on errors."""
669 hook = '%s/%s' % (settings.GetRoot(), hook)
670 if not os.path.exists(hook):
671 return
672 return RunCommand([hook, upstream_branch], error_ok=error_ok,
673 redirect_stdout=False)
674
675
676def CMDpresubmit(parser, args):
677 """run presubmit tests on the current changelist"""
678 parser.add_option('--upload', action='store_true',
679 help='Run upload hook instead of the push/dcommit hook')
680 (options, args) = parser.parse_args(args)
681
682 # Make sure index is up-to-date before running diff-index.
683 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
684 if RunGit(['diff-index', 'HEAD']):
685 # TODO(maruel): Is this really necessary?
686 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
687 return 1
688
689 if args:
690 base_branch = args[0]
691 else:
692 # Default to diffing against the "upstream" branch.
693 base_branch = Changelist().GetUpstreamBranch()
694
695 if options.upload:
696 print '*** Presubmit checks for UPLOAD would report: ***'
697 return not RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch,
698 error_ok=True)
699 else:
700 print '*** Presubmit checks for DCOMMIT would report: ***'
701 return not RunHook(PREDCOMMIT_HOOK, upstream_branch=base_branch,
702 error_ok=True)
703
704
705@usage('[args to "git diff"]')
706def CMDupload(parser, args):
707 """upload the current changelist to codereview"""
708 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
709 help='bypass upload presubmit hook')
710 parser.add_option('-m', dest='message', help='message for patch')
711 parser.add_option('-r', '--reviewers',
712 help='reviewer email addresses')
713 parser.add_option('--send-mail', action='store_true',
714 help='send email to reviewer immediately')
715 parser.add_option("--emulate_svn_auto_props", action="store_true",
716 dest="emulate_svn_auto_props",
717 help="Emulate Subversion's auto properties feature.")
718 parser.add_option("--desc_from_logs", action="store_true",
719 dest="from_logs",
720 help="""Squashes git commit logs into change description and
721 uses message as subject""")
722 (options, args) = parser.parse_args(args)
723
724 # Make sure index is up-to-date before running diff-index.
725 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
726 if RunGit(['diff-index', 'HEAD']):
727 print 'Cannot upload with a dirty tree. You must commit locally first.'
728 return 1
729
730 cl = Changelist()
731 if args:
732 base_branch = args[0]
733 else:
734 # Default to diffing against the "upstream" branch.
735 base_branch = cl.GetUpstreamBranch()
736 args = [base_branch + "..."]
737
738 if not options.bypass_hooks:
739 RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch, error_ok=False)
740
741 # --no-ext-diff is broken in some versions of Git, so try to work around
742 # this by overriding the environment (but there is still a problem if the
743 # git config key "diff.external" is used).
744 env = os.environ.copy()
745 if 'GIT_EXTERNAL_DIFF' in env:
746 del env['GIT_EXTERNAL_DIFF']
747 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
748 env=env)
749
750 upload_args = ['--assume_yes'] # Don't ask about untracked files.
751 upload_args.extend(['--server', cl.GetRietveldServer()])
752 if options.reviewers:
753 upload_args.extend(['--reviewers', options.reviewers])
754 upload_args.extend(['--cc', settings.GetCCList()])
755 if options.emulate_svn_auto_props:
756 upload_args.append('--emulate_svn_auto_props')
757 if options.send_mail:
758 if not options.reviewers:
759 DieWithError("Must specify reviewers to send email.")
760 upload_args.append('--send_mail')
761 if options.from_logs and not options.message:
762 print 'Must set message for subject line if using desc_from_logs'
763 return 1
764
765 change_desc = None
766
767 if cl.GetIssue():
768 if options.message:
769 upload_args.extend(['--message', options.message])
770 upload_args.extend(['--issue', cl.GetIssue()])
771 print ("This branch is associated with issue %s. "
772 "Adding patch to that issue." % cl.GetIssue())
773 else:
774 log_desc = CreateDescriptionFromLog(args)
775 if options.from_logs:
776 # Uses logs as description and message as subject.
777 subject = options.message
778 change_desc = subject + '\n\n' + log_desc
779 else:
780 initial_text = """# Enter a description of the change.
781# This will displayed on the codereview site.
782# The first line will also be used as the subject of the review.
783"""
784 if 'BUG=' not in log_desc:
785 log_desc += '\nBUG='
786 if 'TEST=' not in log_desc:
787 log_desc += '\nTEST='
788 change_desc = UserEditedLog(initial_text + log_desc)
789 subject = ''
790 if change_desc:
791 subject = change_desc.splitlines()[0]
792 if not change_desc:
793 print "Description is empty; aborting."
794 return 1
795 upload_args.extend(['--message', subject])
796 upload_args.extend(['--description', change_desc])
797
798 # Include the upstream repo's URL in the change -- this is useful for
799 # projects that have their source spread across multiple repos.
800 remote_url = None
801 if settings.GetIsGitSvn():
802 data = RunGit(['svn', 'info'])
803 if data:
804 keys = dict(line.split(': ', 1) for line in data.splitlines()
805 if ': ' in line)
806 remote_url = keys.get('URL', None)
807 else:
808 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
809 remote_url = (cl.GetRemoteUrl() + '@'
810 + cl.GetUpstreamBranch().split('/')[-1])
811 if remote_url:
812 upload_args.extend(['--base_url', remote_url])
813
814 try:
815 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
816 except:
817 # If we got an exception after the user typed a description for their
818 # change, back up the description before re-raising.
819 if change_desc:
820 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
821 print '\nGot exception while uploading -- saving description to %s\n' \
822 % backup_path
823 backup_file = open(backup_path, 'w')
824 backup_file.write(change_desc)
825 backup_file.close()
826 raise
827
828 if not cl.GetIssue():
829 cl.SetIssue(issue)
830 cl.SetPatchset(patchset)
831 return 0
832
833
834def SendUpstream(parser, args, cmd):
835 """Common code for CmdPush and CmdDCommit
836
837 Squashed commit into a single.
838 Updates changelog with metadata (e.g. pointer to review).
839 Pushes/dcommits the code upstream.
840 Updates review and closes.
841 """
842 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
843 help='bypass upload presubmit hook')
844 parser.add_option('-m', dest='message',
845 help="override review description")
846 parser.add_option('-f', action='store_true', dest='force',
847 help="force yes to questions (don't prompt)")
848 parser.add_option('-c', dest='contributor',
849 help="external contributor for patch (appended to " +
850 "description and used as author for git). Should be " +
851 "formatted as 'First Last <email@example.com>'")
852 parser.add_option('--tbr', action='store_true', dest='tbr',
853 help="short for 'to be reviewed', commit branch " +
854 "even without uploading for review")
855 (options, args) = parser.parse_args(args)
856 cl = Changelist()
857
858 if not args or cmd == 'push':
859 # Default to merging against our best guess of the upstream branch.
860 args = [cl.GetUpstreamBranch()]
861
862 base_branch = args[0]
863
chase@chromium.orgc76e6752011-01-10 18:17:12 +0000864 # Make sure index is up-to-date before running diff-index.
865 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866 if RunGit(['diff-index', 'HEAD']):
867 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
868 return 1
869
870 # This rev-list syntax means "show all commits not in my branch that
871 # are in base_branch".
872 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
873 base_branch]).splitlines()
874 if upstream_commits:
875 print ('Base branch "%s" has %d commits '
876 'not in this branch.' % (base_branch, len(upstream_commits)))
877 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
878 return 1
879
880 if cmd == 'dcommit':
881 # This is the revision `svn dcommit` will commit on top of.
882 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
883 '--pretty=format:%H'])
884 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
885 if extra_commits:
886 print ('This branch has %d additional commits not upstreamed yet.'
887 % len(extra_commits.splitlines()))
888 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
889 'before attempting to %s.' % (base_branch, cmd))
890 return 1
891
892 if not options.force and not options.bypass_hooks:
893 RunHook(PREDCOMMIT_HOOK, upstream_branch=base_branch, error_ok=False)
894
895 if cmd == 'dcommit':
896 # Check the tree status if the tree status URL is set.
897 status = GetTreeStatus()
898 if 'closed' == status:
899 print ('The tree is closed. Please wait for it to reopen. Use '
900 '"git cl dcommit -f" to commit on a closed tree.')
901 return 1
902 elif 'unknown' == status:
903 print ('Unable to determine tree status. Please verify manually and '
904 'use "git cl dcommit -f" to commit on a closed tree.')
905
906 description = options.message
907 if not options.tbr:
908 # It is important to have these checks early. Not only for user
909 # convenience, but also because the cl object then caches the correct values
910 # of these fields even as we're juggling branches for setting up the commit.
911 if not cl.GetIssue():
912 print 'Current issue unknown -- has this branch been uploaded?'
913 print 'Use --tbr to commit without review.'
914 return 1
915
916 if not description:
917 description = cl.GetDescription()
918
919 if not description:
920 print 'No description set.'
921 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
922 return 1
923
924 description += "\n\nReview URL: %s" % cl.GetIssueURL()
925 else:
926 if not description:
927 # Submitting TBR. See if there's already a description in Rietveld, else
928 # create a template description. Eitherway, give the user a chance to edit
929 # it to fill in the TBR= field.
930 if cl.GetIssue():
931 description = cl.GetDescription()
932
933 if not description:
934 description = """# Enter a description of the change.
935# This will be used as the change log for the commit.
936
937"""
938 description += CreateDescriptionFromLog(args)
939
940 description = UserEditedLog(description + '\nTBR=')
941
942 if not description:
943 print "Description empty; aborting."
944 return 1
945
946 if options.contributor:
947 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
948 print "Please provide contibutor as 'First Last <email@example.com>'"
949 return 1
950 description += "\nPatch from %s." % options.contributor
951 print 'Description:', repr(description)
952
953 branches = [base_branch, cl.GetBranchRef()]
954 if not options.force:
955 subprocess.call(['git', 'diff', '--stat'] + branches)
956 raw_input("About to commit; enter to confirm.")
957
958 # We want to squash all this branch's commits into one commit with the
959 # proper description.
960 # We do this by doing a "merge --squash" into a new commit branch, then
961 # dcommitting that.
962 MERGE_BRANCH = 'git-cl-commit'
963 # Delete the merge branch if it already exists.
964 if RunGitWithCode(['show-ref', '--quiet', '--verify',
965 'refs/heads/' + MERGE_BRANCH])[0] == 0:
966 RunGit(['branch', '-D', MERGE_BRANCH])
967
968 # We might be in a directory that's present in this branch but not in the
969 # trunk. Move up to the top of the tree so that git commands that expect a
970 # valid CWD won't fail after we check out the merge branch.
971 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
972 if rel_base_path:
973 os.chdir(rel_base_path)
974
975 # Stuff our change into the merge branch.
976 # We wrap in a try...finally block so if anything goes wrong,
977 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +0000978 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979 try:
980 RunGit(['checkout', '-q', '-b', MERGE_BRANCH, base_branch])
981 RunGit(['merge', '--squash', cl.GetBranchRef()])
982 if options.contributor:
983 RunGit(['commit', '--author', options.contributor, '-m', description])
984 else:
985 RunGit(['commit', '-m', description])
986 if cmd == 'push':
987 # push the merge branch.
988 remote, branch = cl.FetchUpstreamTuple()
989 retcode, output = RunGitWithCode(
990 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
991 logging.debug(output)
992 else:
993 # dcommit the merge branch.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +0000994 retcode, output = RunGitWithCode(['svn', 'dcommit', '--no-rebase'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 finally:
996 # And then swap back to the original branch and clean up.
997 RunGit(['checkout', '-q', cl.GetBranch()])
998 RunGit(['branch', '-D', MERGE_BRANCH])
999
1000 if cl.GetIssue():
1001 if cmd == 'dcommit' and 'Committed r' in output:
1002 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1003 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001004 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1005 for l in output.splitlines(False))
1006 match = filter(None, match)
1007 if len(match) != 1:
1008 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1009 output)
1010 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 else:
1012 return 1
1013 viewvc_url = settings.GetViewVCUrl()
1014 if viewvc_url and revision:
1015 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1016 print ('Closing issue '
1017 '(you may be prompted for your codereview password)...')
1018 cl.CloseIssue()
1019 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001020
1021 if retcode == 0:
1022 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1023 if os.path.isfile(hook):
1024 RunHook(hook, upstream_branch=base_branch, error_ok=True)
1025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 return 0
1027
1028
1029@usage('[upstream branch to apply against]')
1030def CMDdcommit(parser, args):
1031 """commit the current changelist via git-svn"""
1032 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001033 message = """This doesn't appear to be an SVN repository.
1034If your project has a git mirror with an upstream SVN master, you probably need
1035to run 'git svn init', see your project's git mirror documentation.
1036If your project has a true writeable upstream repository, you probably want
1037to run 'git cl push' instead.
1038Choose wisely, if you get this wrong, your commit might appear to succeed but
1039will instead be silently ignored."""
1040 print(message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 raw_input('[Press enter to dcommit or ctrl-C to quit]')
1042 return SendUpstream(parser, args, 'dcommit')
1043
1044
1045@usage('[upstream branch to apply against]')
1046def CMDpush(parser, args):
1047 """commit the current changelist via git"""
1048 if settings.GetIsGitSvn():
1049 print('This appears to be an SVN repository.')
1050 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
1051 raw_input('[Press enter to push or ctrl-C to quit]')
1052 return SendUpstream(parser, args, 'push')
1053
1054
1055@usage('<patch url or issue id>')
1056def CMDpatch(parser, args):
1057 """patch in a code review"""
1058 parser.add_option('-b', dest='newbranch',
1059 help='create a new branch off trunk for the patch')
1060 parser.add_option('-f', action='store_true', dest='force',
1061 help='with -b, clobber any existing branch')
1062 parser.add_option('--reject', action='store_true', dest='reject',
1063 help='allow failed patches and spew .rej files')
1064 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1065 help="don't commit after patch applies")
1066 (options, args) = parser.parse_args(args)
1067 if len(args) != 1:
1068 parser.print_help()
1069 return 1
1070 input = args[0]
1071
1072 if re.match(r'\d+', input):
1073 # Input is an issue id. Figure out the URL.
1074 issue = input
1075 server = settings.GetDefaultServerUrl()
1076 fetch = urllib2.urlopen('%s/%s' % (server, issue)).read()
1077 m = re.search(r'/download/issue[0-9]+_[0-9]+.diff', fetch)
1078 if not m:
1079 DieWithError('Must pass an issue ID or full URL for '
1080 '\'Download raw patch set\'')
1081 url = '%s%s' % (server, m.group(0).strip())
1082 else:
1083 # Assume it's a URL to the patch. Default to http.
1084 input = FixUrl(input)
1085 match = re.match(r'.*?/issue(\d+)_\d+.diff', input)
1086 if match:
1087 issue = match.group(1)
1088 url = input
1089 else:
1090 DieWithError('Must pass an issue ID or full URL for '
1091 '\'Download raw patch set\'')
1092
1093 if options.newbranch:
1094 if options.force:
1095 RunGit(['branch', '-D', options.newbranch],
1096 swallow_stderr=True, error_ok=True)
1097 RunGit(['checkout', '-b', options.newbranch,
1098 Changelist().GetUpstreamBranch()])
1099
1100 # Switch up to the top-level directory, if necessary, in preparation for
1101 # applying the patch.
1102 top = RunGit(['rev-parse', '--show-cdup']).strip()
1103 if top:
1104 os.chdir(top)
1105
1106 patch_data = urllib2.urlopen(url).read()
1107 # Git patches have a/ at the beginning of source paths. We strip that out
1108 # with a sed script rather than the -p flag to patch so we can feed either
1109 # Git or svn-style patches into the same apply command.
1110 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1111 sed_proc = Popen(['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'],
1112 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
1113 patch_data = sed_proc.communicate(patch_data)[0]
1114 if sed_proc.returncode:
1115 DieWithError('Git patch mungling failed.')
1116 logging.info(patch_data)
1117 # We use "git apply" to apply the patch instead of "patch" so that we can
1118 # pick up file adds.
1119 # The --index flag means: also insert into the index (so we catch adds).
1120 cmd = ['git', 'apply', '--index', '-p0']
1121 if options.reject:
1122 cmd.append('--reject')
1123 patch_proc = Popen(cmd, stdin=subprocess.PIPE)
1124 patch_proc.communicate(patch_data)
1125 if patch_proc.returncode:
1126 DieWithError('Failed to apply the patch')
1127
1128 # If we had an issue, commit the current state and register the issue.
1129 if not options.nocommit:
1130 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1131 cl = Changelist()
1132 cl.SetIssue(issue)
1133 print "Committed patch."
1134 else:
1135 print "Patch applied to index."
1136 return 0
1137
1138
1139def CMDrebase(parser, args):
1140 """rebase current branch on top of svn repo"""
1141 # Provide a wrapper for git svn rebase to help avoid accidental
1142 # git svn dcommit.
1143 # It's the only command that doesn't use parser at all since we just defer
1144 # execution to git-svn.
1145 RunGit(['svn', 'rebase'] + args, redirect_stdout=False)
1146 return 0
1147
1148
1149def GetTreeStatus():
1150 """Fetches the tree status and returns either 'open', 'closed',
1151 'unknown' or 'unset'."""
1152 url = settings.GetTreeStatusUrl(error_ok=True)
1153 if url:
1154 status = urllib2.urlopen(url).read().lower()
1155 if status.find('closed') != -1 or status == '0':
1156 return 'closed'
1157 elif status.find('open') != -1 or status == '1':
1158 return 'open'
1159 return 'unknown'
1160
1161 return 'unset'
1162
1163def GetTreeStatusReason():
1164 """Fetches the tree status from a json url and returns the message
1165 with the reason for the tree to be opened or closed."""
1166 # Don't import it at file level since simplejson is not installed by default
1167 # on python 2.5 and it is only used for git-cl tree which isn't often used,
1168 # forcing everyone to install simplejson isn't efficient.
1169 try:
1170 import simplejson as json
1171 except ImportError:
1172 try:
1173 import json
1174 # Some versions of python2.5 have an incomplete json module. Check to make
1175 # sure loads exists.
1176 json.loads
1177 except (ImportError, AttributeError):
1178 print >> sys.stderr, 'Please install simplejson'
1179 sys.exit(1)
1180
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001181 url = settings.GetTreeStatusUrl()
1182 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 connection = urllib2.urlopen(json_url)
1184 status = json.loads(connection.read())
1185 connection.close()
1186 return status['message']
1187
1188def CMDtree(parser, args):
1189 """show the status of the tree"""
1190 (options, args) = parser.parse_args(args)
1191 status = GetTreeStatus()
1192 if 'unset' == status:
1193 print 'You must configure your tree status URL by running "git cl config".'
1194 return 2
1195
1196 print "The tree is %s" % status
1197 print
1198 print GetTreeStatusReason()
1199 if status != 'open':
1200 return 1
1201 return 0
1202
1203
1204def CMDupstream(parser, args):
1205 """print the name of the upstream branch, if any"""
1206 (options, args) = parser.parse_args(args)
1207 cl = Changelist()
1208 print cl.GetUpstreamBranch()
1209 return 0
1210
1211
1212def Command(name):
1213 return getattr(sys.modules[__name__], 'CMD' + name, None)
1214
1215
1216def CMDhelp(parser, args):
1217 """print list of commands or help for a specific command"""
1218 (options, args) = parser.parse_args(args)
1219 if len(args) == 1:
1220 return main(args + ['--help'])
1221 parser.print_help()
1222 return 0
1223
1224
1225def GenUsage(parser, command):
1226 """Modify an OptParse object with the function's documentation."""
1227 obj = Command(command)
1228 more = getattr(obj, 'usage_more', '')
1229 if command == 'help':
1230 command = '<command>'
1231 else:
1232 # OptParser.description prefer nicely non-formatted strings.
1233 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1234 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1235
1236
1237def main(argv):
1238 """Doesn't parse the arguments here, just find the right subcommand to
1239 execute."""
1240 # Do it late so all commands are listed.
1241 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1242 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1243 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1244
1245 # Create the option parse and add --verbose support.
1246 parser = optparse.OptionParser()
1247 parser.add_option('-v', '--verbose', action='store_true')
1248 old_parser_args = parser.parse_args
1249 def Parse(args):
1250 options, args = old_parser_args(args)
1251 if options.verbose:
1252 logging.basicConfig(level=logging.DEBUG)
1253 else:
1254 logging.basicConfig(level=logging.WARNING)
1255 return options, args
1256 parser.parse_args = Parse
1257
1258 if argv:
1259 command = Command(argv[0])
1260 if command:
1261 # "fix" the usage and the description now that we know the subcommand.
1262 GenUsage(parser, argv[0])
1263 try:
1264 return command(parser, argv[1:])
1265 except urllib2.HTTPError, e:
1266 if e.code != 500:
1267 raise
1268 DieWithError(
1269 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1270 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1271
1272 # Not a known command. Default to help.
1273 GenUsage(parser, 'help')
1274 return CMDhelp(parser, argv)
1275
1276
1277if __name__ == '__main__':
1278 sys.exit(main(sys.argv[1:]))