cmd_stage: recurse through directories
[yap.git] / yap / yap.py
blob3da37e1b2b16577ca431eed9983127c8c89c802a
1 import sys
2 import os
3 import glob
4 import getopt
5 import pickle
6 import tempfile
8 from util import *
10 class ShellError(Exception):
11 def __init__(self, cmd, rc):
12 self.cmd = cmd
13 self.rc = rc
15 def __str__(self):
16 return "%s returned %d" % (self.cmd, self.rc)
18 class YapError(Exception):
19 def __init__(self, msg):
20 self.msg = msg
22 def __str__(self):
23 return self.msg
25 class YapCore(object):
26 def _add_new_file(self, file):
27 repo = get_output('git rev-parse --git-dir')[0]
28 dir = os.path.join(repo, 'yap')
29 try:
30 os.mkdir(dir)
31 except OSError:
32 pass
33 files = self._get_new_files()
34 files.append(file)
35 path = os.path.join(dir, 'new-files')
36 pickle.dump(files, open(path, 'w'))
38 def _get_new_files(self):
39 repo = get_output('git rev-parse --git-dir')[0]
40 path = os.path.join(repo, 'yap', 'new-files')
41 try:
42 files = pickle.load(file(path))
43 except IOError:
44 files = []
46 x = []
47 for f in files:
48 # if f in the index
49 if get_output("git ls-files --cached '%s'" % f) != []:
50 continue
51 x.append(f)
52 return x
54 def _remove_new_file(self, file):
55 files = self._get_new_files()
56 files = filter(lambda x: x != file, files)
58 repo = get_output('git rev-parse --git-dir')[0]
59 path = os.path.join(repo, 'yap', 'new-files')
60 try:
61 pickle.dump(files, open(path, 'w'))
62 except IOError:
63 pass
65 def _assert_file_exists(self, file):
66 if not os.access(file, os.R_OK):
67 raise YapError("No such file: %s" % file)
69 def _repo_path_to_rel(self, path):
70 prefix = get_output("git rev-parse --show-prefix")
71 if not prefix:
72 return path
74 prefix = [ prefix[0] ]
75 while True:
76 head, tail = os.path.split(prefix[0])
77 if not head:
78 break
79 prefix[0] = head
80 if tail:
81 prefix.insert(1, tail)
83 path = [ path ]
84 while True:
85 head, tail = os.path.split(path[0])
86 if not head:
87 break
88 path[0] = head
89 if tail:
90 path.insert(1, tail)
92 common = 0
93 for a, b in zip(prefix, path):
94 if a != b:
95 break
96 common += 1
98 path = path[common:]
99 cdup = [".."] * (len(prefix) - common)
100 path = cdup + list(path)
101 path = os.path.join(*path)
102 return path
104 def _get_staged_files(self):
105 if run_command("git rev-parse HEAD"):
106 files = get_output("git ls-files --cached")
107 else:
108 files = get_output("git diff-index --cached --name-only HEAD")
109 unmerged = self._get_unmerged_files()
110 if unmerged:
111 unmerged = set(unmerged)
112 files = set(files).difference(unmerged)
113 files = list(files)
114 return files
116 def _get_unstaged_files(self):
117 cwd = os.getcwd()
118 cdup = self._get_cdup()
119 os.chdir(cdup)
120 files = get_output("git ls-files -m")
121 os.chdir(cwd)
123 new_files = self._get_new_files()
124 if new_files:
125 staged = self._get_staged_files()
126 if staged:
127 staged = set(staged)
128 new_files = set(new_files).difference(staged)
129 new_files = list(new_files)
130 files += new_files
131 unmerged = self._get_unmerged_files()
132 if unmerged:
133 unmerged = set(unmerged)
134 files = set(files).difference(unmerged)
135 files = list(files)
136 return files
138 def _get_unmerged_files(self):
139 cwd = os.getcwd()
140 cdup = self._get_cdup()
141 os.chdir(cdup)
142 files = get_output("git ls-files -u")
143 os.chdir(cwd)
144 files = [ x.replace('\t', ' ').split(' ')[3] for x in files ]
145 return list(set(files))
147 def _resolve_rev(self, rev):
148 ref = get_output("git rev-parse --verify %s 2>/dev/null" % rev)
149 if not ref:
150 raise YapError("No such revision: %s" % rev)
151 return ref[0]
153 def _delete_branch(self, branch, force):
154 current = get_output("git symbolic-ref HEAD")
155 if current:
156 current = current[0].replace('refs/heads/', '')
157 if branch == current:
158 raise YapError("Can't delete current branch")
160 ref = self._resolve_rev('refs/heads/'+branch)
161 run_safely("git update-ref -d 'refs/heads/%s' '%s'" % (branch, ref))
163 if not force:
164 name = get_output("git name-rev --name-only '%s'" % ref)[0]
165 if name == 'undefined':
166 run_command("git update-ref 'refs/heads/%s' '%s'" % (branch, ref))
167 raise YapError("Refusing to delete leaf branch (use -f to force)")
168 def _get_pager_cmd(self):
169 if 'YAP_PAGER' in os.environ:
170 return os.environ['YAP_PAGER']
171 elif 'GIT_PAGER' in os.environ:
172 return os.environ['GIT_PAGER']
173 elif 'PAGER' in os.environ:
174 return os.environ['PAGER']
175 else:
176 return "less"
178 def _add_one(self, file):
179 self._assert_file_exists(file)
180 x = get_output("git ls-files '%s'" % file)
181 if x != [] or file in self._get_new_files():
182 raise YapError("File '%s' already in repository" % file)
183 self._add_new_file(file)
185 def _rm_one(self, file):
186 self._assert_file_exists(file)
187 if get_output("git ls-files '%s'" % file) != []:
188 run_safely("git rm --cached '%s'" % file)
189 self._remove_new_file(file)
191 def _stage_one(self, file, allow_unmerged=False):
192 self._assert_file_exists(file)
193 prefix = get_output("git rev-parse --show-prefix")
194 if prefix:
195 tmp = os.path.normpath(os.path.join(prefix[0], file))
196 else:
197 tmp = file
198 if not allow_unmerged and tmp in self._get_unmerged_files():
199 raise YapError("Refusing to stage conflicted file: %s" % file)
200 run_safely("git update-index --add '%s'" % file)
202 def _get_cdup(self):
203 cdup = get_output("git rev-parse --show-cdup")
204 assert cdup
205 if cdup[0]:
206 cdup = cdup[0]
207 else:
208 cdup = '.'
209 return cdup
211 def _unstage_one(self, file):
212 self._assert_file_exists(file)
213 if run_command("git rev-parse HEAD"):
214 rc = run_command("git update-index --force-remove '%s'" % file)
215 else:
216 cdup = self._get_cdup()
217 rc = run_command("git diff-index --cached -p HEAD '%s' | (cd %s; git apply -R --cached)" % (file, cdup))
218 if rc:
219 raise YapError("Failed to unstage")
221 def _revert_one(self, file):
222 self._assert_file_exists(file)
223 try:
224 self._unstage_one(file)
225 except YapError:
226 pass
227 run_safely("git checkout-index -u -f '%s'" % file)
229 def _parse_commit(self, commit):
230 lines = get_output("git cat-file commit '%s'" % commit)
231 commit = {}
233 mode = None
234 for l in lines:
235 if mode != 'commit' and l.strip() == "":
236 mode = 'commit'
237 commit['log'] = []
238 continue
239 if mode == 'commit':
240 commit['log'].append(l)
241 continue
243 x = l.split(' ')
244 k = x[0]
245 v = ' '.join(x[1:])
246 commit[k] = v
247 commit['log'] = '\n'.join(commit['log'])
248 return commit
250 def _check_commit(self, **flags):
251 if '-a' in flags and '-d' in flags:
252 raise YapError("Conflicting flags: -a and -d")
254 if '-d' not in flags and self._get_unstaged_files():
255 if '-a' not in flags and self._get_staged_files():
256 raise YapError("Staged and unstaged changes present. Specify what to commit")
257 cdup = self._get_cdup()
258 run_command("(cd %s; git add -u)" % cdup)
259 for f in self._get_new_files():
260 self._stage_one(f)
262 def _do_uncommit(self):
263 commit = self._parse_commit("HEAD")
264 repo = get_output('git rev-parse --git-dir')[0]
265 dir = os.path.join(repo, 'yap')
266 try:
267 os.mkdir(dir)
268 except OSError:
269 pass
270 msg_file = os.path.join(dir, 'msg')
271 fd = file(msg_file, 'w')
272 print >>fd, commit['log']
273 fd.close()
275 tree = get_output("git rev-parse --verify HEAD^")
276 run_safely("git update-ref -m uncommit HEAD '%s'" % tree[0])
278 def _do_commit(self, msg=None):
279 tree = get_output("git write-tree")[0]
281 repo = get_output('git rev-parse --git-dir')[0]
282 head_file = os.path.join(repo, 'yap', 'merge')
283 try:
284 parent = pickle.load(file(head_file))
285 except IOError:
286 parent = get_output("git rev-parse --verify HEAD 2> /dev/null")
288 if os.environ.has_key('YAP_EDITOR'):
289 editor = os.environ['YAP_EDITOR']
290 elif os.environ.has_key('GIT_EDITOR'):
291 editor = os.environ['GIT_EDITOR']
292 elif os.environ.has_key('EDITOR'):
293 editor = os.environ['EDITOR']
294 else:
295 editor = "vi"
297 fd, tmpfile = tempfile.mkstemp("yap")
298 os.close(fd)
300 if msg is None:
301 msg_file = os.path.join(repo, 'yap', 'msg')
302 if os.access(msg_file, os.R_OK):
303 fd1 = file(msg_file)
304 fd2 = file(tmpfile, 'w')
305 for l in fd1.xreadlines():
306 print >>fd2, l.strip()
307 fd2.close()
308 os.unlink(msg_file)
309 if os.system("%s '%s'" % (editor, tmpfile)) != 0:
310 raise YapError("Editing commit message failed")
311 fd = file(tmpfile)
312 msg = fd.readlines()
313 msg = ''.join(msg)
315 msg = msg.strip()
316 if not msg:
317 raise YapError("Refusing to use empty commit message")
319 fd = os.popen("git stripspace > %s" % tmpfile, 'w')
320 print >>fd, msg,
321 fd.close()
323 if parent:
324 parent = ' -p '.join(parent)
325 commit = get_output("git commit-tree '%s' -p %s < '%s'" % (tree, parent, tmpfile))
326 else:
327 commit = get_output("git commit-tree '%s' < '%s'" % (tree, tmpfile))
329 os.unlink(tmpfile)
330 run_safely("git update-ref HEAD '%s'" % commit[0])
331 self._clear_state()
333 def _check_rebasing(self):
334 repo = get_output('git rev-parse --git-dir')[0]
335 dotest = os.path.join(repo, '.dotest')
336 if os.access(dotest, os.R_OK):
337 raise YapError("A git operation is in progress. Complete it first")
338 dotest = os.path.join(repo, '..', '.dotest')
339 if os.access(dotest, os.R_OK):
340 raise YapError("A git operation is in progress. Complete it first")
342 def _check_git(self):
343 if run_command("git rev-parse --git-dir"):
344 raise YapError("That command must be run from inside a git repository")
346 def _list_remotes(self):
347 remotes = get_output("git config --get-regexp '^remote.*.url'")
348 for x in remotes:
349 remote, url = x.split(' ')
350 remote = remote.replace('remote.', '')
351 remote = remote.replace('.url', '')
352 yield remote, url
354 def _unstage_all(self):
355 try:
356 run_safely("git read-tree -m HEAD")
357 except ShellError:
358 run_safely("git read-tree HEAD")
359 run_safely("git update-index -q --refresh")
361 def _get_tracking(self, current):
362 remote = get_output("git config branch.%s.remote" % current)
363 if not remote:
364 raise YapError("No tracking branch configured for '%s'" % current)
366 merge = get_output("git config branch.%s.merge" % current)
367 if not merge:
368 raise YapError("No tracking branch configured for '%s'" % current)
369 return remote[0], merge[0]
371 def _confirm_push(self, current, rhs, repo):
372 print "About to push local branch '%s' to '%s' on '%s'" % (current, rhs, repo)
373 print "Continue (y/n)? ",
374 sys.stdout.flush()
375 ans = sys.stdin.readline().strip()
377 if ans.lower() != 'y' and ans.lower() != 'yes':
378 raise YapError("Aborted.")
380 def _clear_state(self):
381 repo = get_output('git rev-parse --git-dir')[0]
382 dir = os.path.join(repo, 'yap')
383 for f in "new-files", "merge", "msg":
384 try:
385 os.unlink(os.path.join(dir, f))
386 except OSError:
387 pass
389 def _get_attr(self, name, attr):
390 val = None
391 for c in self.__class__.__bases__:
392 try:
393 m2 = c.__dict__[name]
394 except KeyError:
395 continue
396 try:
397 val = m2.__getattribute__(attr)
398 except AttributeError:
399 continue
400 return val
402 def _filter_log(self, commit):
403 return commit
405 def _check_rename(self, rev, path):
406 renames = get_output("git diff-tree -C -M --diff-filter=R %s %s^"
407 % (rev, rev))
408 for r in renames:
409 r = r.replace('\t', ' ')
410 fields = r.split(' ')
411 mode1, mode2, hash1, hash2, rename, dst, src = fields
412 if dst == path:
413 return src
414 return None
416 @short_help("make a local copy of an existing repository")
417 @long_help("""
418 The first argument is a URL to the existing repository. This can be an
419 absolute path if the repository is local, or a URL with the git://,
420 ssh://, or http:// schemes. By default, the directory used is the last
421 component of the URL, sans '.git'. This can be overridden by providing
422 a second argument.
423 """)
424 def cmd_clone(self, url, directory=None):
425 "<url> [directory]"
427 if '://' not in url and url[0] != '/':
428 url = os.path.join(os.getcwd(), url)
430 url = url.rstrip('/')
431 if directory is None:
432 directory = url.rsplit('/')[-1]
433 directory = directory.replace('.git', '')
435 try:
436 os.mkdir(directory)
437 except OSError:
438 raise YapError("Directory exists: %s" % directory)
439 os.chdir(directory)
440 self.cmd_init()
441 self.cmd_repo("origin", url)
442 self.cmd_fetch("origin")
444 branch = None
445 if not run_command("git rev-parse --verify refs/remotes/origin/HEAD"):
446 hash = get_output("git rev-parse refs/remotes/origin/HEAD")[0]
447 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
448 if get_output("git rev-parse %s" % b)[0] == hash:
449 branch = b
450 break
451 if branch is None:
452 if not run_command("git rev-parse --verify refs/remotes/origin/master"):
453 branch = "refs/remotes/origin/master"
454 if branch is None:
455 branch = get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'")
456 branch = branch[0]
458 hash = get_output("git rev-parse %s" % branch)
459 assert hash
460 branch = branch.replace('refs/remotes/origin/', '')
461 run_safely("git update-ref refs/heads/%s %s" % (branch, hash[0]))
462 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch)
463 self.cmd_revert(**{'-a': 1})
465 @short_help("turn a directory into a repository")
466 @long_help("""
467 Converts the current working directory into a repository. The primary
468 side-effect of this command is the creation of a '.git' subdirectory.
469 No files are added nor commits made.
470 """)
471 def cmd_init(self):
472 os.system("git init")
474 @short_help("add a new file to the repository")
475 @long_help("""
476 The arguments are the files to be added to the repository. Once added,
477 the files will show as "unstaged changes" in the output of 'status'. To
478 reverse the effects of this command, see 'rm'.
479 """)
480 def cmd_add(self, *files):
481 "<file>..."
482 self._check_git()
484 if not files:
485 raise TypeError
487 for f in files:
488 self._add_one(f)
489 self.cmd_status()
491 @short_help("delete a file from the repository")
492 @long_help("""
493 The arguments are the files to be removed from the current revision of
494 the repository. The files will still exist in any past commits that the
495 files may have been a part of. The file is not actually deleted, it is
496 just no longer tracked as part of the repository.
497 """)
498 def cmd_rm(self, *files):
499 "<file>..."
500 self._check_git()
501 if not files:
502 raise TypeError
504 for f in files:
505 self._rm_one(f)
506 self.cmd_status()
508 @short_help("stage changes in a file for commit")
509 @long_help("""
510 The arguments are the files to be staged. Staging changes is a way to
511 build up a commit when you do not want to commit all changes at once.
512 To commit only staged changes, use the '-d' flag to 'commit.' To
513 reverse the effects of this command, see 'unstage'. Once staged, the
514 files will show as "staged changes" in the output of 'status'.
515 """)
516 def cmd_stage(self, *files):
517 "<file>..."
518 self._check_git()
519 if not files:
520 raise TypeError
522 files = list(files)
523 for i, f in enumerate(files[:]):
524 if not os.path.isdir(f):
525 continue
527 del files[i]
528 fd = os.popen("find %s -type f" % f)
529 for x in fd.xreadlines():
530 x = x.strip()
531 if '.git' in x.split(os.path.sep):
532 continue
533 files.append(x)
535 for f in files:
536 self._stage_one(f)
537 self.cmd_status()
539 @short_help("unstage changes in a file")
540 @long_help("""
541 The arguments are the files to be unstaged. Once unstaged, the files
542 will show as "unstaged changes" in the output of 'status'. The '-a'
543 flag can be used to unstage all staged changes at once.
544 """)
545 @takes_options("a")
546 def cmd_unstage(self, *files, **flags):
547 "[-a] | <file>..."
548 self._check_git()
549 if '-a' in flags:
550 files = self._get_staged_files()
552 if not files:
553 raise YapError("Nothing to do")
555 files = [ self._repo_path_to_rel(x) for x in files ]
556 for f in files:
557 self._unstage_one(f)
558 self.cmd_status()
560 @short_help("show files with staged and unstaged changes")
561 @long_help("""
562 Show the files in the repository with changes since the last commit,
563 categorized based on whether the changes are staged or not. A file may
564 appear under each heading if the same file has both staged and unstaged
565 changes.
566 """)
567 def cmd_status(self):
569 self._check_git()
570 branch = get_output("git symbolic-ref HEAD")
571 if branch:
572 branch = branch[0].replace('refs/heads/', '')
573 else:
574 branch = "DETACHED"
575 print "Current branch: %s" % branch
577 print "Files with staged changes:"
578 files = self._get_staged_files()
579 for f in files:
580 print "\t%s" % self._repo_path_to_rel(f)
581 if not files:
582 print "\t(none)"
584 print "Files with unstaged changes:"
585 files = self._get_unstaged_files()
586 for f in files:
587 print "\t%s" % self._repo_path_to_rel(f)
588 if not files:
589 print "\t(none)"
591 files = self._get_unmerged_files()
592 if files:
593 print "Files with conflicts:"
594 for f in files:
595 print "\t%s" % self._repo_path_to_rel(f)
597 @short_help("remove uncommitted changes from a file (*)")
598 @long_help("""
599 The arguments are the files whose changes will be reverted. If the '-a'
600 flag is given, then all files will have uncommitted changes removed.
601 Note that there is no way to reverse this command short of manually
602 editing each file again.
603 """)
604 @takes_options("a")
605 def cmd_revert(self, *files, **flags):
606 "(-a | <file>)"
607 self._check_git()
608 if '-a' in flags:
609 cdup = self._get_cdup()
610 run_command("(cd %s; git add -u)" % cdup)
611 os.system("git read-tree -v --aggressive -u -m HEAD")
612 self._clear_state()
613 self.cmd_status()
614 return
616 if not files:
617 raise TypeError
619 for f in files:
620 self._revert_one(f)
621 self.cmd_status()
623 @short_help("record changes to files as a new commit")
624 @long_help("""
625 Create a new commit recording changes since the last commit. If there
626 are only unstaged changes, those will be recorded. If there are only
627 staged changes, those will be recorded. Otherwise, you will have to
628 specify either the '-a' flag or the '-d' flag to commit all changes or
629 only staged changes, respectively. To reverse the effects of this
630 command, see 'uncommit'.
631 """)
632 @takes_options("adm:")
633 def cmd_commit(self, **flags):
634 "[-a | -d] [-m <msg>]"
635 self._check_git()
636 self._check_rebasing()
637 self._check_commit(**flags)
638 if not self._get_staged_files():
639 raise YapError("No changes to commit")
640 msg = flags.get('-m', None)
641 self._do_commit(msg)
642 self.cmd_status()
644 @short_help("reverse the actions of the last commit")
645 @long_help("""
646 Reverse the effects of the last 'commit' operation. The changes that
647 were part of the previous commit will show as "staged changes" in the
648 output of 'status'. This means that if no files were changed since the
649 last commit was created, 'uncommit' followed by 'commit' is a lossless
650 operation.
651 """)
652 def cmd_uncommit(self):
654 self._check_git()
655 self._do_uncommit()
656 self.cmd_status()
658 @short_help("report the current version of yap")
659 def cmd_version(self):
660 print "Yap version %s" % self.version
662 @short_help("show the changelog for particular versions or files")
663 @long_help("""
664 The arguments are the files with which to filter history. If none are
665 given, all changes are listed. Otherwise only commits that affected one
666 or more of the given files are listed. The -r option changes the
667 starting revision for traversing history. By default, history is listed
668 starting at HEAD.
669 """)
670 @takes_options("pr:")
671 def cmd_log(self, *paths, **flags):
672 "[-p] [-r <rev>] <path>..."
673 self._check_git()
674 rev = flags.get('-r', 'HEAD')
675 rev = self._resolve_rev(rev)
676 paths = list(paths)
678 if '-p' in flags:
679 flags['-p'] = '-p'
681 try:
682 pager = os.popen(self._get_pager_cmd(), 'w')
683 rename = False
684 while True:
685 for hash in yield_output("git rev-list '%s' -- %s"
686 % (rev, ' '.join(paths))):
687 commit = get_output("git show -M -C %s %s"
688 % (flags.get('-p', '--name-status'), hash),
689 strip=False)
690 commit = self._filter_log(commit)
691 print >>pager, ''.join(commit)
693 # Check for renames
694 if len(paths) == 1:
695 src = self._check_rename(hash, paths[0])
696 if src is not None:
697 paths[0] = src
698 rename = True
699 rev = hash+"^"
700 break
701 if not rename:
702 break
703 rename = False
704 except (IOError, KeyboardInterrupt):
705 pass
707 @short_help("show staged, unstaged, or all uncommitted changes")
708 @long_help("""
709 Show staged, unstaged, or all uncommitted changes. By default, all
710 changes are shown. The '-u' flag causes only unstaged changes to be
711 shown. The '-d' flag causes only staged changes to be shown.
712 """)
713 @takes_options("ud")
714 def cmd_diff(self, **flags):
715 "[ -u | -d ]"
716 self._check_git()
717 if '-u' in flags and '-d' in flags:
718 raise YapError("Conflicting flags: -u and -d")
720 pager = self._get_pager_cmd()
722 if '-u' in flags:
723 os.system("git diff-files -p | %s" % pager)
724 elif '-d' in flags:
725 os.system("git diff-index --cached -p HEAD | %s" % pager)
726 else:
727 os.system("git diff-index -p HEAD | %s" % pager)
729 @short_help("list, create, or delete branches")
730 @long_help("""
731 If no arguments are specified, a list of local branches is given. The
732 current branch is indicated by a "*" next to the name. If an argument
733 is given, it is taken as the name of a new branch to create. The branch
734 will start pointing at the current HEAD. See 'point' for details on
735 changing the revision of the new branch. Note that this command does
736 not switch the current working branch. See 'switch' for details on
737 changing the current working branch.
739 The '-d' flag can be used to delete local branches. If the delete
740 operation would remove the last branch reference to a given line of
741 history (colloquially referred to as "dangling commits"), yap will
742 report an error and abort. The '-f' flag can be used to force the delete
743 in spite of this.
744 """)
745 @takes_options("fd:")
746 def cmd_branch(self, branch=None, **flags):
747 "[ [-f] -d <branch> | <branch> ]"
748 self._check_git()
749 force = '-f' in flags
750 if '-d' in flags:
751 self._delete_branch(flags['-d'], force)
752 self.cmd_branch()
753 return
755 if branch is not None:
756 ref = get_output("git rev-parse --verify HEAD")
757 if not ref:
758 raise YapError("No branch point yet. Make a commit")
759 run_safely("git update-ref 'refs/heads/%s' '%s'" % (branch, ref[0]))
761 current = get_output("git symbolic-ref HEAD")
762 branches = get_output("git for-each-ref --format='%(refname)' 'refs/heads'")
763 for b in branches:
764 if current and b == current[0]:
765 print "* ",
766 else:
767 print " ",
768 b = b.replace('refs/heads/', '')
769 print b
771 @short_help("change the current working branch")
772 @long_help("""
773 The argument is the name of the branch to make the current working
774 branch. This command will fail if there are uncommitted changes to any
775 files. Otherwise, the contents of the files in the working directory
776 are updated to reflect their state in the new branch. Additionally, any
777 future commits are added to the new branch instead of the previous line
778 of history.
779 """)
780 @takes_options("f")
781 def cmd_switch(self, branch, **flags):
782 "[-f] <branch>"
783 self._check_git()
784 self._check_rebasing()
785 ref = self._resolve_rev('refs/heads/'+branch)
787 if '-f' not in flags:
788 if (self._get_staged_files()
789 or (self._get_unstaged_files()
790 and run_command("git update-index --refresh"))):
791 raise YapError("You have uncommitted changes. Use -f to continue anyway")
793 if self._get_unstaged_files() and self._get_staged_files():
794 raise YapError("You have staged and unstaged changes. Perhaps unstage -a?")
796 staged = bool(self._get_staged_files())
798 cdup = self._get_cdup()
799 run_command("(cd %s; git add -u)" % cdup)
800 for f in self._get_new_files():
801 self._stage_one(f)
803 idx = get_output("git write-tree")
804 new = self._resolve_rev('refs/heads/'+branch)
806 run_command("git update-index --refresh")
807 readtree = "git read-tree -v --aggressive -u -m HEAD %s %s" % (idx[0], new)
808 if os.system(readtree):
809 raise YapError("Failed to switch")
810 run_safely("git symbolic-ref HEAD refs/heads/%s" % branch)
812 if '-f' not in flags:
813 self._clear_state()
815 if not staged:
816 self._unstage_all()
817 self.cmd_status()
819 @short_help("move the current branch to a different revision")
820 @long_help("""
821 The argument is the hash of the commit to which the current branch
822 should point, or alternately a branch or tag (a.k.a, "committish"). If
823 moving the branch would create "dangling commits" (see 'branch'), yap
824 will report an error and abort. The '-f' flag can be used to force the
825 operation in spite of this.
826 """)
827 @takes_options("f")
828 def cmd_point(self, where, **flags):
829 "[-f] <where>"
830 self._check_git()
831 self._check_rebasing()
833 head = get_output("git rev-parse --verify HEAD")
834 if not head:
835 raise YapError("No commit yet; nowhere to point")
837 ref = self._resolve_rev(where)
838 ref = get_output("git rev-parse --verify '%s^{commit}'" % ref)
839 if not ref:
840 raise YapError("Not a commit: %s" % where)
842 if self._get_unstaged_files() or self._get_staged_files():
843 raise YapError("You have uncommitted changes. Commit them first")
845 run_safely("git update-ref HEAD '%s'" % ref[0])
847 if '-f' not in flags:
848 name = get_output("git name-rev --name-only '%s'" % head[0])[0]
849 if name == "undefined":
850 os.system("git update-ref HEAD '%s'" % head[0])
851 raise YapError("Pointing there will lose commits. Use -f to force")
853 run_command("git update-index --refresh")
854 rc = os.system("git read-tree -v --reset -u HEAD")
855 if rc:
856 raise YapError("checkout-index failed")
857 self._clear_state()
859 @short_help("alter history by dropping or amending commits")
860 @long_help("""
861 This command operates in two distinct modes, "amend" and "drop" mode.
862 In drop mode, the given commit is removed from the history of the
863 current branch, as though that commit never happened. By default the
864 commit used is HEAD.
866 In amend mode, the uncommitted changes present are merged into a
867 previous commit. This is useful for correcting typos or adding missed
868 files into past commits. By default the commit used is HEAD.
870 While rewriting history it is possible that conflicts will arise. If
871 this happens, the rewrite will pause and you will be prompted to resolve
872 the conflicts and stage them. Once that is done, you will run "yap
873 history continue." If instead you want the conflicting commit removed
874 from history (perhaps your changes supercede that commit) you can run
875 "yap history skip". Once the rewrite completes, your branch will be on
876 the same commit as when the rewrite started.
877 """)
878 def cmd_history(self, subcmd, *args):
879 "amend | drop <commit>"
880 self._check_git()
882 if subcmd not in ("amend", "drop", "continue", "skip"):
883 raise TypeError
885 resolvemsg = """
886 When you have resolved the conflicts run \"yap history continue\".
887 To skip the problematic patch, run \"yap history skip\"."""
889 if subcmd == "continue":
890 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
891 return
892 if subcmd == "skip":
893 os.system("git reset --hard")
894 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
895 return
897 if subcmd == "amend":
898 flags, args = getopt.getopt(args, "ad")
899 flags = dict(flags)
901 if len(args) > 1:
902 raise TypeError
903 if args:
904 commit = args[0]
905 else:
906 commit = "HEAD"
908 self._resolve_rev(commit)
909 self._check_rebasing()
911 if subcmd == "amend":
912 self._check_commit(**flags)
913 if self._get_unstaged_files():
914 # XXX: handle unstaged changes better
915 raise YapError("Commit away changes that you aren't amending")
917 self._unstage_all()
919 start = get_output("git rev-parse HEAD")
920 stash = get_output("git stash create")
921 run_command("git reset --hard")
922 try:
923 fd, tmpfile = tempfile.mkstemp("yap")
924 try:
925 try:
926 os.close(fd)
927 os.system("git format-patch -k --stdout '%s' > %s" % (commit, tmpfile))
928 if subcmd == "amend":
929 self.cmd_point(commit, **{'-f': True})
930 finally:
931 if subcmd == "amend":
932 if stash:
933 rc = os.system("git stash apply %s" % stash[0])
934 if rc:
935 self.cmd_point(start[0], **{'-f': True})
936 os.system("git stash apply %s" % stash[0])
937 raise YapError("Failed to apply stash")
938 stash = None
940 if subcmd == "amend":
941 self._do_uncommit()
942 self._check_commit(**{'-a': True})
943 self._do_commit()
944 else:
945 self.cmd_point("%s^" % commit, **{'-f': True})
947 stat = os.stat(tmpfile)
948 size = stat[6]
949 if size > 0:
950 run_safely("git update-index --refresh")
951 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
952 if (rc):
953 raise YapError("Failed to apply changes")
954 finally:
955 os.unlink(tmpfile)
956 finally:
957 if stash:
958 run_command("git stash apply %s" % stash[0])
959 self.cmd_status()
961 @short_help("show the changes introduced by a given commit")
962 @long_help("""
963 By default, the changes in the last commit are shown. To override this,
964 specify a hash, branch, or tag (committish). The hash of the commit,
965 the commit's author, log message, and a diff of the changes are shown.
966 """)
967 def cmd_show(self, commit="HEAD"):
968 "[commit]"
969 self._check_git()
970 commit = self._resolve_rev(commit)
971 os.system("git show '%s'" % commit)
973 @short_help("apply the changes in a given commit to the current branch")
974 @long_help("""
975 The argument is the hash, branch, or tag (committish) of the commit to
976 be applied. In general, it only makes sense to apply commits that
977 happened on another branch. The '-r' flag can be used to have the
978 changes in the given commit reversed from the current branch. In
979 general, this only makes sense for commits that happened on the current
980 branch.
981 """)
982 @takes_options("r")
983 def cmd_cherry_pick(self, commit, **flags):
984 "[-r] <commit>"
985 self._check_git()
986 commit = self._resolve_rev(commit)
987 if '-r' in flags:
988 os.system("git revert '%s'" % commit)
989 else:
990 os.system("git cherry-pick '%s'" % commit)
992 @short_help("list, add, or delete configured remote repositories")
993 @long_help("""
994 When invoked with no arguments, this command will show the list of
995 currently configured remote repositories, giving both the name and URL
996 of each. To add a new repository, give the desired name as the first
997 argument and the URL as the second. The '-d' flag can be used to remove
998 a previously added repository.
999 """)
1000 @takes_options("d:")
1001 def cmd_repo(self, name=None, url=None, **flags):
1002 "[<name> <url> | -d <name>]"
1003 self._check_git()
1004 if name is not None and url is None:
1005 raise TypeError
1007 if '-d' in flags:
1008 if flags['-d'] not in [ x[0] for x in self._list_remotes() ]:
1009 raise YapError("No such repository: %s" % flags['-d'])
1010 os.system("git config --unset remote.%s.url" % flags['-d'])
1011 os.system("git config --unset remote.%s.fetch" % flags['-d'])
1012 for b in get_output("git for-each-ref --format='%(refname)' 'refs/remotes/origin'"):
1013 hash = get_output("git rev-parse %s" % b)
1014 assert hash
1015 run_safely("git update-ref -d %s %s" % (b, hash[0]))
1017 if name:
1018 if name in [ x[0] for x in self._list_remotes() ]:
1019 raise YapError("Repository '%s' already exists" % name)
1020 os.system("git config remote.%s.url %s" % (name, url))
1021 os.system("git config remote.%s.fetch +refs/heads/*:refs/remotes/%s/*" % (name, name))
1023 for remote, url in self._list_remotes():
1024 print "%s" % remote
1025 print " URL: %s" % url
1026 first = True
1027 for b in get_output("git for-each-ref --format='%%(refname)' 'refs/remotes/%s'" % remote):
1028 b = b.replace('refs/remotes/', '')
1029 if first:
1030 branches = "Branches: "
1031 else:
1032 branches = " "
1033 print " %s%s" % (branches, b)
1034 first = False
1036 @short_help("send local commits to a remote repository (*)")
1037 @long_help("""
1038 When invoked with no arguments, the current branch is synchronized to
1039 the tracking branch of the tracking remote. If no tracking remote is
1040 specified, the repository will have to be specified on the command line.
1041 In that case, the default is to push to a branch with the same name as
1042 the current branch. This behavior can be overridden by giving a second
1043 argument to specify the remote branch.
1045 If the remote branch does not currently exist, the command will abort
1046 unless the -c flag is provided. If the remote branch is not a direct
1047 descendent of the local branch, the command will abort unless the -f
1048 flag is provided. Forcing a push in this way can be problematic to
1049 other users of the repository if they are not expecting it.
1051 To delete a branch on the remote repository, use the -d flag.
1052 """)
1053 @takes_options("cdf")
1054 def cmd_push(self, repo=None, rhs=None, **flags):
1055 "[-c | -d] <repo>"
1056 self._check_git()
1057 if '-c' in flags and '-d' in flags:
1058 raise TypeError
1060 if repo and repo not in [ x[0] for x in self._list_remotes() ]:
1061 raise YapError("No such repository: %s" % repo)
1063 current = get_output("git symbolic-ref HEAD")
1064 if not current:
1065 raise YapError("Not on a branch!")
1067 self._check_rebasing()
1069 current = current[0].replace('refs/heads/', '')
1070 remote = get_output("git config branch.%s.remote" % current)
1071 if repo is None and remote:
1072 repo = remote[0]
1074 if repo is None:
1075 raise YapError("No tracking branch configured; specify destination repository")
1077 if rhs is None and remote and remote[0] == repo:
1078 merge = get_output("git config branch.%s.merge" % current)
1079 if merge:
1080 rhs = merge[0]
1082 if rhs is None:
1083 rhs = "refs/heads/%s" % current
1085 if '-c' not in flags and '-d' not in flags:
1086 if run_command("git rev-parse --verify refs/remotes/%s/%s"
1087 % (repo, rhs.replace('refs/heads/', ''))):
1088 raise YapError("No matching branch on that repo. Use -c to create a new branch there.")
1089 if '-f' not in flags:
1090 hash = get_output("git rev-parse refs/remotes/%s/%s" % (repo, rhs.replace('refs/heads/', '')))
1091 base = get_output("git merge-base HEAD %s" % hash[0])
1092 assert base
1093 if base[0] != hash[0]:
1094 raise YapError("Branch not up-to-date with remote. Update or use -f")
1096 self._confirm_push(current, rhs, repo)
1097 if '-f' in flags:
1098 flags['-f'] = '-f'
1100 if '-d' in flags:
1101 lhs = ""
1102 else:
1103 lhs = "refs/heads/%s" % current
1104 rc = os.system("git push %s %s %s:%s" % (flags.get('-f', ''), repo, lhs, rhs))
1105 if rc:
1106 raise YapError("Push failed.")
1108 @short_help("retrieve commits from a remote repository")
1109 @long_help("""
1110 When run with no arguments, the command will retrieve new commits from
1111 the remote tracking repository. Note that this does not in any way
1112 alter the current branch. For that, see "update". If a remote other
1113 than the tracking remote is desired, it can be specified as the first
1114 argument.
1115 """)
1116 def cmd_fetch(self, repo=None):
1117 "<repo>"
1118 self._check_git()
1119 current = get_output("git symbolic-ref HEAD")
1120 if not current:
1121 raise YapError("Not on a branch!")
1123 if repo and repo not in [ x[0] for x in self._list_remotes() ]:
1124 raise YapError("No such repository: %s" % repo)
1125 if repo is None:
1126 current = current[0].replace('refs/heads/', '')
1127 remote = get_output("git config branch.%s.remote" % current)
1128 if remote:
1129 repo = remote[0]
1130 if repo is None:
1131 raise YapError("No tracking branch configured; specify a repository")
1132 os.system("git fetch %s" % repo)
1134 @short_help("update the current branch relative to its tracking branch")
1135 @long_help("""
1136 Updates the current branch relative to its remote tracking branch. This
1137 command requires that the current branch have a remote tracking branch
1138 configured. If any conflicts occur while applying your changes to the
1139 updated remote, the command will pause to allow you to fix them. Once
1140 that is done, run "update" with the "continue" subcommand. Alternately,
1141 the "skip" subcommand can be used to discard the conflicting changes.
1142 """)
1143 def cmd_update(self, subcmd=None):
1144 "[continue | skip]"
1145 self._check_git()
1146 if subcmd and subcmd not in ["continue", "skip"]:
1147 raise TypeError
1149 resolvemsg = """
1150 When you have resolved the conflicts run \"yap update continue\".
1151 To skip the problematic patch, run \"yap update skip\"."""
1153 if subcmd == "continue":
1154 os.system("git am -3 -r --resolvemsg='%s'" % resolvemsg)
1155 return
1156 if subcmd == "skip":
1157 os.system("git reset --hard")
1158 os.system("git am -3 --skip --resolvemsg='%s'" % resolvemsg)
1159 return
1161 self._check_rebasing()
1162 if self._get_unstaged_files() or self._get_staged_files():
1163 raise YapError("You have uncommitted changes. Commit them first")
1165 current = get_output("git symbolic-ref HEAD")
1166 if not current:
1167 raise YapError("Not on a branch!")
1169 current = current[0].replace('refs/heads/', '')
1170 remote, merge = self._get_tracking(current)
1171 merge = merge.replace('refs/heads/', '')
1173 self.cmd_fetch(remote)
1174 base = get_output("git merge-base HEAD refs/remotes/%s/%s" % (remote, merge))
1176 try:
1177 fd, tmpfile = tempfile.mkstemp("yap")
1178 os.close(fd)
1179 os.system("git format-patch -k --stdout '%s' > %s" % (base[0], tmpfile))
1180 self.cmd_point("refs/remotes/%s/%s" % (remote, merge), **{'-f': True})
1182 stat = os.stat(tmpfile)
1183 size = stat[6]
1184 if size > 0:
1185 rc = os.system("git am -3 --resolvemsg=\'%s\' %s" % (resolvemsg, tmpfile))
1186 if (rc):
1187 raise YapError("Failed to apply changes")
1188 finally:
1189 os.unlink(tmpfile)
1191 @short_help("query and configure remote branch tracking")
1192 @long_help("""
1193 When invoked with no arguments, the command displays the tracking
1194 information for the current branch. To configure the tracking
1195 information, two arguments for the remote repository and remote branch
1196 are given. The tracking information is used to provide defaults for
1197 where to push local changes and from where to get updates to the branch.
1198 """)
1199 def cmd_track(self, repo=None, branch=None):
1200 "[<repo> <branch>]"
1201 self._check_git()
1203 current = get_output("git symbolic-ref HEAD")
1204 if not current:
1205 raise YapError("Not on a branch!")
1206 current = current[0].replace('refs/heads/', '')
1208 if repo is None and branch is None:
1209 repo, merge = self._get_tracking(current)
1210 merge = merge.replace('refs/heads/', '')
1211 print "Branch '%s' tracking refs/remotes/%s/%s" % (current, repo, merge)
1212 return
1214 if repo is None or branch is None:
1215 raise TypeError
1217 if repo not in [ x[0] for x in self._list_remotes() ]:
1218 raise YapError("No such repository: %s" % repo)
1220 if run_command("git rev-parse --verify refs/remotes/%s/%s" % (repo, branch)):
1221 raise YapError("No such branch '%s' on repository '%s'" % (branch, repo))
1223 os.system("git config branch.%s.remote '%s'" % (current, repo))
1224 os.system("git config branch.%s.merge 'refs/heads/%s'" % (current, branch))
1225 print "Branch '%s' now tracking refs/remotes/%s/%s" % (current, repo, branch)
1227 @short_help("mark files with conflicts as resolved")
1228 @long_help("""
1229 The arguments are the files to be marked resolved. When a conflict
1230 occurs while merging changes to a file, that file is marked as
1231 "unmerged." Until the file(s) with conflicts are marked resolved,
1232 commits cannot be made.
1233 """)
1234 def cmd_resolved(self, *files):
1235 "<file>..."
1236 self._check_git()
1237 if not files:
1238 raise TypeError
1240 for f in files:
1241 self._stage_one(f, True)
1242 self.cmd_status()
1244 @short_help("merge a branch into the current branch")
1245 def cmd_merge(self, branch):
1246 "<branch>"
1247 self._check_git()
1249 branch_name = branch
1250 branch = self._resolve_rev(branch)
1251 base = get_output("git merge-base HEAD %s" % branch)
1252 if not base:
1253 raise YapError("Branch '%s' is not a fork of the current branch"
1254 % branch)
1256 readtree = ("git read-tree --aggressive -u -m %s HEAD %s"
1257 % (base[0], branch))
1258 if run_command(readtree):
1259 run_command("git update-index --refresh")
1260 if os.system(readtree):
1261 raise YapError("Failed to merge")
1263 repo = get_output('git rev-parse --git-dir')[0]
1264 dir = os.path.join(repo, 'yap')
1265 try:
1266 os.mkdir(dir)
1267 except OSError:
1268 pass
1269 msg_file = os.path.join(dir, 'msg')
1270 msg = file(msg_file, 'w')
1271 print >>msg, "Merge branch '%s'" % branch_name
1272 msg.close()
1274 head = get_output("git rev-parse --verify HEAD")
1275 assert head
1276 heads = [head[0], branch]
1277 head_file = os.path.join(dir, 'merge')
1278 pickle.dump(heads, file(head_file, 'w'))
1280 self._merge_index(branch, base[0])
1281 if self._get_unmerged_files():
1282 self.cmd_status()
1283 raise YapError("Fix conflicts then commit")
1285 self._do_commit()
1287 def _merge_index(self, branch, base):
1288 for f in self._get_unmerged_files():
1289 fd, bfile = tempfile.mkstemp("yap")
1290 os.close(fd)
1291 rc = os.system("git show %s:%s > %s" % (base, f, bfile))
1292 assert rc == 0
1294 fd, ofile = tempfile.mkstemp("yap")
1295 os.close(fd)
1296 rc = os.system("git show %s:%s > %s" % (branch, f, ofile))
1297 assert rc == 0
1299 command = "git merge-file -L %(file)s -L %(file)s.base -L %(file)s.%(branch)s %(file)s %(base)s %(other)s " % dict(file=f, branch=branch, base=bfile, other=ofile)
1300 rc = os.system(command)
1301 os.unlink(ofile)
1302 os.unlink(bfile)
1304 assert rc >= 0
1305 if rc == 0:
1306 self._stage_one(f, True)
1308 def cmd_help(self, cmd=None):
1309 if cmd is not None:
1310 oldcmd = cmd
1311 cmd = "cmd_" + cmd.replace('-', '_')
1312 try:
1313 attr = self.__getattribute__(cmd)
1314 except AttributeError:
1315 raise YapError("No such command: %s" % cmd)
1317 help = self._get_attr(cmd, "long_help")
1318 if help is None:
1319 raise YapError("Sorry, no help for '%s'. Ask Steven." % oldcmd)
1321 print >>sys.stderr, "The '%s' command" % oldcmd
1322 doc = self._get_attr(cmd, "__doc__")
1323 if doc is None:
1324 doc = ""
1325 print >>sys.stderr, "\tyap %s %s" % (oldcmd, doc)
1326 print >>sys.stderr, "%s" % help
1327 return
1329 print >> sys.stderr, "Yet Another (Git) Porcelein"
1330 print >> sys.stderr
1332 for name in dir(self):
1333 if not name.startswith('cmd_'):
1334 continue
1335 attr = self.__getattribute__(name)
1336 if not callable(attr):
1337 continue
1339 short_msg = self._get_attr(name, "short_help")
1340 if short_msg is None:
1341 continue
1343 name = name.replace('cmd_', '')
1344 name = name.replace('_', '-')
1345 print >> sys.stderr, "%-16s%s" % (name, short_msg)
1347 print >> sys.stderr
1348 print >> sys.stderr, "(*) Indicates that the command is not readily reversible"
1350 @short_help("show information about loaded plugins")
1351 def cmd_plugins(self):
1353 print >> sys.stderr, "Loaded plugins:"
1354 plugins = load_plugins()
1355 for name, cls in plugins.items():
1356 print "\t%-16s: %s" % (name, cls.__doc__)
1357 if not plugins:
1358 print "\t%-16s" % "None"
1360 def cmd_usage(self):
1361 print >> sys.stderr, "usage: %s <command>" % os.path.basename(sys.argv[0])
1362 print >> sys.stderr, " valid commands: help init clone add rm stage unstage status revert commit uncommit log show diff branch switch point cherry-pick repo track push fetch update history resolved version"
1364 def load_plugins():
1365 plugindir = os.path.join("~", ".yap", "plugins")
1366 plugindir = os.path.expanduser(plugindir)
1367 plugindir = os.path.join(plugindir, "*.py")
1369 plugins = dict()
1370 for p in glob.glob(os.path.expanduser(plugindir)):
1371 plugin = os.path.basename(p).replace('.py', '')
1372 m = __import__(plugin)
1373 for k in dir(m):
1374 cls = m.__dict__[k]
1375 if not type(cls) == type:
1376 continue
1377 if not issubclass(cls, YapCore):
1378 continue
1379 if cls is YapCore:
1380 continue
1381 plugins[k] = cls
1382 return plugins
1384 def yap_metaclass(name, bases, dct):
1385 plugindir = os.path.join("~", ".yap", "plugins")
1386 plugindir = os.path.expanduser(plugindir)
1387 sys.path.insert(0, plugindir)
1389 plugins = set(load_plugins().values())
1390 p2 = plugins.copy()
1391 for cls in plugins:
1392 p2 -= set(cls.__bases__)
1393 plugins = p2
1394 bases = list(plugins) + list(bases)
1395 return type(name, tuple(bases), dct)
1397 class Yap(YapCore):
1398 __metaclass__ = yap_metaclass
1400 def main(self, args):
1401 if len(args) < 1:
1402 self.cmd_usage()
1403 sys.exit(2)
1405 command = args[0]
1406 args = args[1:]
1408 if run_command("git --version"):
1409 print >>sys.stderr, "Failed to run git; is it installed?"
1410 sys.exit(1)
1412 debug = os.getenv('YAP_DEBUG')
1414 try:
1415 command = command.replace('-', '_')
1416 meth = self.__getattribute__("cmd_"+command)
1417 doc = self._get_attr("cmd_"+command, "__doc__")
1419 try:
1420 options = ""
1421 for c in self.__class__.__bases__:
1422 try:
1423 t = c.__dict__["cmd_"+command]
1424 except KeyError:
1425 continue
1426 if "options" in t.__dict__:
1427 options += t.options
1429 if options:
1430 try:
1431 flags, args = getopt.getopt(args, options)
1432 flags = dict(flags)
1433 except getopt.GetoptError, e:
1434 if debug:
1435 raise
1436 print "Usage: %s %s %s" % (os.path.basename(sys.argv[0]), command, doc)
1437 print e
1438 sys.exit(2)
1439 else:
1440 flags = dict()
1442 meth(*args, **flags)
1443 except (TypeError, getopt.GetoptError):
1444 if debug:
1445 raise
1446 print "Usage: %s %s %s" % (os.path.basename(sys.argv[0]), command, doc)
1447 except YapError, e:
1448 if debug:
1449 raise
1450 print >> sys.stderr, e
1451 sys.exit(1)
1452 except AttributeError:
1453 if debug:
1454 raise
1455 self.cmd_usage()
1456 sys.exit(2)