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 for ext
in ['.local', '.older', '.remote']:
532 if os
.path
.isfile(fn
):
535 def __resolved_all():
536 conflicts
= git
.get_conflicts()
538 for filename
in conflicts
:
540 os
.remove(os
.path
.join(git
.base_dir
, 'conflicts'))
542 def resolved(parser
, options
, args
):
548 parser
.error('incorrect number of arguments')
550 conflicts
= git
.get_conflicts()
552 raise MainException
, 'No more conflicts'
553 # check for arguments validity
554 for filename
in args
:
555 if not filename
in conflicts
:
556 raise MainException
, 'No conflicts for "%s"' % filename
558 for filename
in args
:
560 del conflicts
[conflicts
.index(filename
)]
562 # save or remove the conflicts file
564 os
.remove(os
.path
.join(git
.base_dir
, 'conflicts'))
566 f
= file(os
.path
.join(git
.base_dir
, 'conflicts'), 'w+')
567 f
.writelines([line
+ '\n' for line
in conflicts
])
572 'mark a file conflict as solved',
573 '%prog [options] [<file>[ <file>]]',
574 [make_option('-a', '--all',
575 help = 'mark all conflicts as solved',
576 action
= 'store_true')])
579 def series(parser
, options
, args
):
581 parser
.error('incorrect number of arguments')
583 applied
= crt_series
.get_applied()
585 for p
in applied
[0:-1]:
586 if crt_series
.empty_patch(p
):
592 if crt_series
.empty_patch(p
):
597 for p
in crt_series
.get_unapplied():
598 if crt_series
.empty_patch(p
):
605 'print the patch series',
610 def applied(parser
, options
, args
):
612 parser
.error('incorrect number of arguments')
614 for p
in crt_series
.get_applied():
619 'print the applied patches',
624 def unapplied(parser
, options
, args
):
626 parser
.error('incorrect number of arguments')
628 for p
in crt_series
.get_unapplied():
633 'print the unapplied patches',
638 def top(parser
, options
, args
):
640 parser
.error('incorrect number of arguments')
642 name
= crt_series
.get_current()
646 raise MainException
, 'No patches applied'
650 'print the name of the top patch',
655 def export(parser
, options
, args
):
661 parser
.error('incorrect number of arguments')
663 if git
.local_changes():
664 print 'Warning: local changes in the tree. ' \
665 'You might want to commit them first'
667 if not os
.path
.isdir(dirname
):
669 series
= file(os
.path
.join(dirname
, 'series'), 'w+')
671 patches
= crt_series
.get_applied()
673 zpadding
= len(str(num
))
681 pname
= '%s.diff' % pname
683 pname
= '%s-%s' %
(str(patch_no
).zfill(zpadding
), pname
)
684 pfile
= os
.path
.join(dirname
, pname
)
685 print >> series
, pname
689 patch_tmpl
= options
.template
691 patch_tmpl
= os
.path
.join(git
.base_dir
, 'patchexport.tmpl')
692 if os
.path
.isfile(patch_tmpl
):
693 tmpl
= file(patch_tmpl
).read()
697 # get the patch description
698 patch
= crt_series
.get_patch(p
)
700 tmpl_dict
= {'description': patch
.get_description().rstrip(),
701 'diffstat': git
.diffstat(rev1
= __git_id('%s/bottom' % p
),
702 rev2
= __git_id('%s/top' % p
)),
703 'authname': patch
.get_authname(),
704 'authemail': patch
.get_authemail(),
705 'authdate': patch
.get_authdate(),
706 'commname': patch
.get_commname(),
707 'commemail': patch
.get_commemail()}
708 for key
in tmpl_dict
:
709 if not tmpl_dict
[key
]:
713 descr
= tmpl % tmpl_dict
714 except KeyError, err
:
715 raise MainException
, 'Unknown patch template variable: %s' \
718 raise MainException
, 'Only "%(name)s" variables are ' \
719 'supported in the patch template'
720 f
= open(pfile
, 'w+')
725 git
.diff(rev1
= __git_id('%s/bottom' % p
),
726 rev2
= __git_id('%s/top' % p
),
727 output
= pfile
, append
= True)
734 'exports a series of patches to <dir> (or patches)',
735 '%prog [options] [<dir>]',
736 [make_option('-n', '--numbered',
737 help = 'number the patch names',
738 action
= 'store_true'),
739 make_option('-d', '--diff',
740 help = 'append .diff to the patch names',
741 action
= 'store_true'),
742 make_option('-t', '--template', metavar
= 'FILE',
743 help = 'Use FILE as a template')])
752 'status': status_cmd
,
756 'delete': delete_cmd
,
759 'resolved': resolved_cmd
,
760 'series': series_cmd
,
761 'applied': applied_cmd
,
762 'unapplied':unapplied_cmd
,
764 'refresh': refresh_cmd
,
765 'export': export_cmd
,
769 print 'usage: %s <command> [options]' % os
.path
.basename(sys
.argv
[0])
772 print ' help print this message'
774 cmds
= commands
.keys()
777 print ' ' + cmd
+ ' ' * (12 - len(cmd
)) + commands
[cmd
].help
780 # The main function (command dispatcher)
787 prog
= os
.path
.basename(sys
.argv
[0])
789 if len(sys
.argv
) < 2:
790 print >> sys
.stderr
, 'Unknown command'
791 print >> sys
.stderr
, \
792 ' Try "%s help" for a list of supported commands' % prog
797 if cmd
in ['-h', '--help', 'help']:
800 if cmd
in ['-v', '--version']:
801 print '%s %s' %
(prog
, version
)
803 if not cmd
in commands
:
804 print >> sys
.stderr
, 'Unknown command: %s' % cmd
805 print >> sys
.stderr
, ' Try "%s help" for a list of supported commands' \
809 # re-build the command line arguments
810 sys
.argv
[0] += ' %s' % cmd
813 command
= commands
[cmd
]
814 parser
= OptionParser(usage
= command
.usage
,
815 option_list
= command
.option_list
)
816 options
, args
= parser
.parse_args()
818 crt_series
= stack
.Series()
819 command
.func(parser
, options
, args
)
820 except (IOError, MainException
, stack
.StackException
, git
.GitException
), \
822 print >> sys
.stderr
, '%s %s: %s' %
(prog
, cmd
, err
)