1 """Basic quilt-like functionality
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 from optparse import OptionParser, make_option
24 from stgit.utils import *
25 from stgit import stack, git
26 from stgit.version import version
27 from stgit.config import config
30 # Main exception class
31 class MainException(Exception):
42 string_list = string.split('/')
44 if len(string_list) == 1:
46 git_id = string_list[0]
51 return read_string(crt_series.get_base_file())
53 for path in [os.path.join(git.base_dir, 'refs', 'heads'),
54 os.path.join(git.base_dir, 'refs', 'tags')]:
55 id_file = os.path.join(path, git_id)
56 if os.path.isfile(id_file):
57 return read_string(id_file)
58 elif len(string_list) == 2:
59 patch_name = string_list[0]
61 patch_name = crt_series.get_current()
62 git_id = string_list[1]
65 raise MainException, 'No patches applied'
66 elif not (patch_name in crt_series.get_applied()
67 + crt_series.get_unapplied()):
68 raise MainException, 'Unknown patch "%s"' % patch_name
70 if git_id == 'bottom':
71 return crt_series.get_patch(patch_name).get_bottom()
73 return crt_series.get_patch(patch_name).get_top()
75 raise MainException, 'Unknown id: %s' % string
77 def __check_local_changes():
78 if git.local_changes():
79 raise MainException, \
80 'local changes in the tree. Use "refresh" to commit them'
82 def __check_head_top_equal():
83 if not crt_series.head_top_equal():
84 raise MainException, \
85 'HEAD and top are not the same. You probably committed\n' \
86 ' changes to the tree ouside of StGIT. If you know what you\n' \
87 ' are doing, use the "refresh -f" command'
89 def __check_conflicts():
90 if os.path.exists(os.path.join(git.base_dir, 'conflicts')):
91 raise MainException, 'Unsolved conflicts. Please resolve them first'
93 def __print_crt_patch():
94 patch = crt_series.get_current()
96 print 'Now at patch "%s"' % patch
98 print 'No patches applied'
105 """This class is used to store the command details
107 def __init__(self, func, help, usage, option_list):
111 self.option_list = option_list
114 def init(parser, options, args):
115 """Performs the repository initialisation
118 parser.error('incorrect number of arguments')
124 'initialise the tree for use with StGIT',
129 def add(parser, options, args):
130 """Add files or directories to the repository
133 parser.error('incorrect number of arguments')
139 'add files or directories to the repository',
140 '%prog <files/dirs...>',
144 def rm(parser, options, args):
145 """Remove files from the repository
148 parser.error('incorrect number of arguments')
150 git.rm(args, options.force)
154 'remove files from the repository',
155 '%prog [options] <files...>',
156 [make_option('-f', '--force',
157 help = 'force removing even if the file exists',
158 action = 'store_true')])
161 def status(parser, options, args):
162 """Show the tree status
164 git.status(args, options.modified, options.new, options.deleted,
165 options.conflict, options.unknown)
169 'show the tree status',
170 '%prog [options] [<files...>]',
171 [make_option('-m', '--modified',
172 help = 'show modified files only',
173 action = 'store_true'),
174 make_option('-n', '--new',
175 help = 'show new files only',
176 action = 'store_true'),
177 make_option('-d', '--deleted',
178 help = 'show deleted files only',
179 action = 'store_true'),
180 make_option('-c', '--conflict',
181 help = 'show conflict files only',
182 action = 'store_true'),
183 make_option('-u', '--unknown',
184 help = 'show unknown files only',
185 action = 'store_true')])
188 def diff(parser, options, args):
189 """Show the tree diff
192 rev_list = options.revs.split(':')
193 rev_list_len = len(rev_list)
194 if rev_list_len == 1:
195 if rev_list[0][-1] == '/':
197 rev1 = rev_list[0] + 'bottom'
198 rev2 = rev_list[0] + 'top'
202 elif rev_list_len == 2:
208 parser.error('incorrect parameters to -r')
214 print git.diffstat(args, __git_id(rev1), __git_id(rev2))
216 git.diff(args, __git_id(rev1), __git_id(rev2))
220 'show the tree diff',
221 '%prog [options] [<files...>]\n\n'
222 'The revision format is "([patch]/[bottom | top]) | <tree-ish>"',
223 [make_option('-r', metavar = 'rev1[:[rev2]]', dest = 'revs',
224 help = 'show the diff between revisions'),
225 make_option('-s', '--stat',
226 help = 'show the stat instead of the diff',
227 action = 'store_true')])
230 def files(parser, options, args):
231 """Show the files modified by a patch (or the current patch)
238 parser.error('incorrect number of arguments')
240 rev1 = __git_id('%s/bottom' % patch)
241 rev2 = __git_id('%s/top' % patch)
244 print git.diffstat(rev1 = rev1, rev2 = rev2)
246 print git.files(rev1, rev2)
250 'show the files modified by a patch (or the current patch)',
251 '%prog [options] [<patch>]',
252 [make_option('-s', '--stat',
253 help = 'show the diff stat',
254 action = 'store_true')])
257 def refresh(parser, options, args):
259 parser.error('incorrect number of arguments')
261 if config.has_option('stgit', 'autoresolved'):
262 autoresolved = config.get('stgit', 'autoresolved')
266 if autoresolved != 'yes':
269 patch = crt_series.get_current()
271 raise MainException, 'No patches applied'
273 if not options.force:
274 __check_head_top_equal()
276 if git.local_changes() \
277 or not crt_series.head_top_equal() \
278 or options.edit or options.message \
279 or options.authname or options.authemail or options.authdate \
280 or options.commname or options.commemail:
281 print 'Refreshing patch "%s"...' % patch,
284 if autoresolved == 'yes':
286 crt_series.refresh_patch(message = options.message,
288 author_name = options.authname,
289 author_email = options.authemail,
290 author_date = options.authdate,
291 committer_name = options.commname,
292 committer_email = options.commemail)
296 print 'Patch "%s" is already up to date' % patch
300 'generate a new commit for the current patch',
302 [make_option('-f', '--force',
303 help = 'force the refresh even if HEAD and '\
305 action = 'store_true'),
306 make_option('-e', '--edit',
307 help = 'invoke an editor for the patch '\
309 action = 'store_true'),
310 make_option('-m', '--message',
311 help = 'use MESSAGE as the patch ' \
313 make_option('--authname',
314 help = 'use AUTHNAME as the author name'),
315 make_option('--authemail',
316 help = 'use AUTHEMAIL as the author e-mail'),
317 make_option('--authdate',
318 help = 'use AUTHDATE as the author date'),
319 make_option('--commname',
320 help = 'use COMMNAME as the committer name'),
321 make_option('--commemail',
322 help = 'use COMMEMAIL as the committer ' \
326 def new(parser, options, args):
327 """Creates a new patch
330 parser.error('incorrect number of arguments')
332 __check_local_changes()
334 __check_head_top_equal()
336 crt_series.new_patch(args[0], message = options.message,
337 author_name = options.authname,
338 author_email = options.authemail,
339 author_date = options.authdate,
340 committer_name = options.commname,
341 committer_email = options.commemail)
345 'create a new patch and make it the topmost one',
346 '%prog [options] <name>',
347 [make_option('-m', '--message',
348 help = 'use MESSAGE as the patch description'),
349 make_option('--authname',
350 help = 'use AUTHNAME as the author name'),
351 make_option('--authemail',
352 help = 'use AUTHEMAIL as the author e-mail'),
353 make_option('--authdate',
354 help = 'use AUTHDATE as the author date'),
355 make_option('--commname',
356 help = 'use COMMNAME as the committer name'),
357 make_option('--commemail',
358 help = 'use COMMEMAIL as the committer e-mail')])
360 def delete(parser, options, args):
364 parser.error('incorrect number of arguments')
366 if args[0] == crt_series.get_current():
367 __check_local_changes()
369 __check_head_top_equal()
371 crt_series.delete_patch(args[0])
372 print 'Patch "%s" successfully deleted' % args[0]
377 'remove the topmost or any unapplied patch',
382 def push(parser, options, args):
383 """Pushes the given patch or all onto the series
385 # If --undo is passed, do the work and exit
387 patch = crt_series.get_current()
389 raise MainException, 'No patch to undo'
391 print 'Undoing the "%s" push...' % patch,
394 crt_series.undo_push()
400 __check_local_changes()
402 __check_head_top_equal()
404 unapplied = crt_series.get_unapplied()
406 raise MainException, 'No more patches to push'
409 boundaries = options.to.split(':')
410 if len(boundaries) == 1:
411 if boundaries[0] not in unapplied:
412 raise MainException, 'Patch "%s" not unapplied' % boundaries[0]
413 patches = unapplied[:unapplied.index(boundaries[0])+1]
414 elif len(boundaries) == 2:
415 if boundaries[0] not in unapplied:
416 raise MainException, 'Patch "%s" not unapplied' % boundaries[0]
417 if boundaries[1] not in unapplied:
418 raise MainException, 'Patch "%s" not unapplied' % boundaries[1]
419 lb = unapplied.index(boundaries[0])
420 hb = unapplied.index(boundaries[1])
422 raise MainException, 'Patch "%s" after "%s"' \
423 % (boundaries[0], boundaries[1])
424 patches = unapplied[lb:hb+1]
426 raise MainException, 'incorrect parameters to "--to"'
428 patches = unapplied[:options.number]
432 patches = [unapplied[0]]
436 parser.error('incorrect number of arguments')
439 raise MainException, 'No patches to push'
445 print 'Pushing patch "%s"...' % p,
448 crt_series.push_patch(p)
450 if crt_series.empty_patch(p):
451 print 'done (empty patch)'
458 'push a patch on top of the series',
459 '%prog [options] [<name>]',
460 [make_option('-a', '--all',
461 help = 'push all the unapplied patches',
462 action = 'store_true'),
463 make_option('-n', '--number', type = 'int',
464 help = 'push the specified number of patches'),
465 make_option('-t', '--to', metavar = 'PATCH1[:PATCH2]',
466 help = 'push all patches to PATCH1 or between '
467 'PATCH1 and PATCH2'),
468 make_option('--reverse',
469 help = 'push the patches in reverse order',
470 action = 'store_true'),
471 make_option('--undo',
472 help = 'undo the last push operation',
473 action = 'store_true')])
476 def pop(parser, options, args):
478 parser.error('incorrect number of arguments')
480 __check_local_changes()
482 __check_head_top_equal()
484 applied = crt_series.get_applied()
486 raise MainException, 'No patches applied'
490 if options.to not in applied:
491 raise MainException, 'Patch "%s" not applied' % options.to
492 patches = applied[:applied.index(options.to)]
494 patches = applied[:options.number]
498 patches = [applied[0]]
501 raise MainException, 'No patches to pop'
503 # pop everything to the given patch
505 if len(patches) == 1:
506 print 'Popping patch "%s"...' % p,
508 print 'Popping "%s" - "%s" patches...' % (patches[0], p),
511 crt_series.pop_patch(p)
518 'pop the top of the series',
520 [make_option('-a', '--all',
521 help = 'pop all the applied patches',
522 action = 'store_true'),
523 make_option('-n', '--number', type = 'int',
524 help = 'pop the specified number of patches'),
525 make_option('-t', '--to', metavar = 'PATCH',
526 help = 'pop all patches up to PATCH')])
529 def __resolved(filename):
530 git.update_cache([filename])
531 for ext in ['.local', '.older', '.remote']:
533 if os.path.isfile(fn):
536 def __resolved_all():
537 conflicts = git.get_conflicts()
539 for filename in conflicts:
541 os.remove(os.path.join(git.base_dir, 'conflicts'))
543 def resolved(parser, options, args):
549 parser.error('incorrect number of arguments')
551 conflicts = git.get_conflicts()
553 raise MainException, 'No more conflicts'
554 # check for arguments validity
555 for filename in args:
556 if not filename in conflicts:
557 raise MainException, 'No conflicts for "%s"' % filename
559 for filename in args:
561 del conflicts[conflicts.index(filename)]
563 # save or remove the conflicts file
565 os.remove(os.path.join(git.base_dir, 'conflicts'))
567 f = file(os.path.join(git.base_dir, 'conflicts'), 'w+')
568 f.writelines([line + '\n' for line in conflicts])
573 'mark a file conflict as solved',
574 '%prog [options] [<file>[ <file>]]',
575 [make_option('-a', '--all',
576 help = 'mark all conflicts as solved',
577 action = 'store_true')])
580 def series(parser, options, args):
582 parser.error('incorrect number of arguments')
584 applied = crt_series.get_applied()
586 for p in applied [0:-1]:
587 if crt_series.empty_patch(p):
593 if crt_series.empty_patch(p):
598 for p in crt_series.get_unapplied():
599 if crt_series.empty_patch(p):
606 'print the patch series',
611 def applied(parser, options, args):
613 parser.error('incorrect number of arguments')
615 for p in crt_series.get_applied():
620 'print the applied patches',
625 def unapplied(parser, options, args):
627 parser.error('incorrect number of arguments')
629 for p in crt_series.get_unapplied():
634 'print the unapplied patches',
639 def top(parser, options, args):
641 parser.error('incorrect number of arguments')
643 name = crt_series.get_current()
647 raise MainException, 'No patches applied'
651 'print the name of the top patch',
656 def export(parser, options, args):
662 parser.error('incorrect number of arguments')
664 if git.local_changes():
665 print 'Warning: local changes in the tree. ' \
666 'You might want to commit them first'
668 if not os.path.isdir(dirname):
670 series = file(os.path.join(dirname, 'series'), 'w+')
672 applied = crt_series.get_applied()
675 boundaries = options.range.split(':')
676 if len(boundaries) == 1:
677 start = boundaries[0]
679 if len(boundaries) == 2:
680 if boundaries[0] == '':
683 start = boundaries[0]
684 if boundaries[1] == '':
689 raise MainException, 'incorrect parameters to "--range"'
692 start_idx = applied.index(start)
694 raise MainException, 'Patch "%s" not applied' % start
696 stop_idx = applied.index(stop) + 1
698 raise MainException, 'Patch "%s" not applied' % stop
700 if start_idx >= stop_idx:
701 raise MainException, 'Incorrect patch range order'
706 patches = applied[start_idx:stop_idx]
709 zpadding = len(str(num))
717 pname = '%s.diff' % pname
719 pname = '%s-%s' % (str(patch_no).zfill(zpadding), pname)
720 pfile = os.path.join(dirname, pname)
721 print >> series, pname
725 patch_tmpl = options.template
727 patch_tmpl = os.path.join(git.base_dir, 'patchexport.tmpl')
728 if os.path.isfile(patch_tmpl):
729 tmpl = file(patch_tmpl).read()
733 # get the patch description
734 patch = crt_series.get_patch(p)
736 tmpl_dict = {'description': patch.get_description().rstrip(),
737 'diffstat': git.diffstat(rev1 = __git_id('%s/bottom' % p),
738 rev2 = __git_id('%s/top' % p)),
739 'authname': patch.get_authname(),
740 'authemail': patch.get_authemail(),
741 'authdate': patch.get_authdate(),
742 'commname': patch.get_commname(),
743 'commemail': patch.get_commemail()}
744 for key in tmpl_dict:
745 if not tmpl_dict[key]:
749 descr = tmpl % tmpl_dict
750 except KeyError, err:
751 raise MainException, 'Unknown patch template variable: %s' \
754 raise MainException, 'Only "%(name)s" variables are ' \
755 'supported in the patch template'
756 f = open(pfile, 'w+')
761 git.diff(rev1 = __git_id('%s/bottom' % p),
762 rev2 = __git_id('%s/top' % p),
763 output = pfile, append = True)
770 'exports a series of patches to <dir> (or patches)',
771 '%prog [options] [<dir>]',
772 [make_option('-n', '--numbered',
773 help = 'number the patch names',
774 action = 'store_true'),
775 make_option('-d', '--diff',
776 help = 'append .diff to the patch names',
777 action = 'store_true'),
778 make_option('-t', '--template', metavar = 'FILE',
779 help = 'Use FILE as a template'),
780 make_option('-r', '--range',
781 metavar = '[PATCH1][:[PATCH2]]',
782 help = 'export patches between ' \
783 'PATCH1 and PATCH2')])
792 'status': status_cmd,
796 'delete': delete_cmd,
799 'resolved': resolved_cmd,
800 'series': series_cmd,
801 'applied': applied_cmd,
802 'unapplied':unapplied_cmd,
804 'refresh': refresh_cmd,
805 'export': export_cmd,
809 print 'usage: %s <command> [options]' % os.path.basename(sys.argv[0])
812 print ' help print this message'
814 cmds = commands.keys()
817 print ' ' + cmd + ' ' * (12 - len(cmd)) + commands[cmd].help
820 # The main function (command dispatcher)
827 prog = os.path.basename(sys.argv[0])
829 if len(sys.argv) < 2:
830 print >> sys.stderr, 'Unknown command'
831 print >> sys.stderr, \
832 ' Try "%s help" for a list of supported commands' % prog
837 if cmd in ['-h', '--help', 'help']:
840 if cmd in ['-v', '--version']:
841 print '%s %s' % (prog, version)
843 if not cmd in commands:
844 print >> sys.stderr, 'Unknown command: %s' % cmd
845 print >> sys.stderr, ' Try "%s help" for a list of supported commands' \
849 # re-build the command line arguments
850 sys.argv[0] += ' %s' % cmd
853 command = commands[cmd]
854 parser = OptionParser(usage = command.usage,
855 option_list = command.option_list)
856 options, args = parser.parse_args()
858 crt_series = stack.Series()
859 command.func(parser, options, args)
860 except (IOError, MainException, stack.StackException, git.GitException), \
862 print >> sys.stderr, '%s %s: %s' % (prog, cmd, err)