Warn the user when there is no parent information in the config.
[stgit] / stgit / stack.py
1 """Basic quilt-like functionality
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
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.
10
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.
15
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
19 """
20
21 import sys, os, re
22
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
26
27
28 # stack exception class
29 class StackException(Exception):
30 pass
31
32 class FilterUntil:
33 def __init__(self):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
36 if until_test(x):
37 self.should_print = False
38 if self.should_print:
39 return x[0:len(prefix)] != prefix
40 return False
41
42 #
43 # Functions
44 #
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
47
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
50 """
51 f.seek(0)
52
53 # remove status-prefixed lines
54 lines = f.readlines()
55
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
59
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
62 del lines[-1]
63
64 f.seek(0); f.truncate()
65 f.writelines(lines)
66
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
70
71 f = file(fname, 'w+')
72 if line:
73 print >> f, line
74 elif tmpl:
75 print >> f, tmpl,
76 else:
77 print >> f
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
81 % __comment_prefix
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
84
85 if show_patch:
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
89
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92 f.close()
93
94 # the editor
95 editor = config.get('stgit.editor')
96 if editor:
97 pass
98 elif 'EDITOR' in os.environ:
99 editor = os.environ['EDITOR']
100 else:
101 editor = 'vi'
102 editor += ' %s' % fname
103
104 print 'Invoking the editor: "%s"...' % editor,
105 sys.stdout.flush()
106 print 'done (exit code: %d)' % os.system(editor)
107
108 f = file(fname, 'r+')
109
110 __clean_comments(f)
111 f.seek(0)
112 result = f.read()
113
114 f.close()
115 os.remove(fname)
116
117 return result
118
119 #
120 # Classes
121 #
122
123 class StgitObject:
124 """An object with stgit-like properties stored as files in a directory
125 """
126 def _set_dir(self, dir):
127 self.__dir = dir
128 def _dir(self):
129 return self.__dir
130
131 def create_empty_field(self, name):
132 create_empty_file(os.path.join(self.__dir, name))
133
134 def _get_field(self, name, multiline = False):
135 id_file = os.path.join(self.__dir, name)
136 if os.path.isfile(id_file):
137 line = read_string(id_file, multiline)
138 if line == '':
139 return None
140 else:
141 return line
142 else:
143 return None
144
145 def _set_field(self, name, value, multiline = False):
146 fname = os.path.join(self.__dir, name)
147 if value and value != '':
148 write_string(fname, value, multiline)
149 elif os.path.isfile(fname):
150 os.remove(fname)
151
152
153 class Patch(StgitObject):
154 """Basic patch implementation
155 """
156 def __init__(self, name, series_dir, refs_dir):
157 self.__series_dir = series_dir
158 self.__name = name
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_dir = refs_dir
161 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
162 self.__log_ref_file = os.path.join(self.__refs_dir,
163 self.__name + '.log')
164
165 def create(self):
166 os.mkdir(self._dir())
167 self.create_empty_field('bottom')
168 self.create_empty_field('top')
169
170 def delete(self):
171 for f in os.listdir(self._dir()):
172 os.remove(os.path.join(self._dir(), f))
173 os.rmdir(self._dir())
174 os.remove(self.__top_ref_file)
175 if os.path.exists(self.__log_ref_file):
176 os.remove(self.__log_ref_file)
177
178 def get_name(self):
179 return self.__name
180
181 def rename(self, newname):
182 olddir = self._dir()
183 old_top_ref_file = self.__top_ref_file
184 old_log_ref_file = self.__log_ref_file
185 self.__name = newname
186 self._set_dir(os.path.join(self.__series_dir, self.__name))
187 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
188 self.__log_ref_file = os.path.join(self.__refs_dir,
189 self.__name + '.log')
190
191 os.rename(olddir, self._dir())
192 os.rename(old_top_ref_file, self.__top_ref_file)
193 if os.path.exists(old_log_ref_file):
194 os.rename(old_log_ref_file, self.__log_ref_file)
195
196 def __update_top_ref(self, ref):
197 write_string(self.__top_ref_file, ref)
198
199 def __update_log_ref(self, ref):
200 write_string(self.__log_ref_file, ref)
201
202 def update_top_ref(self):
203 top = self.get_top()
204 if top:
205 self.__update_top_ref(top)
206
207 def get_old_bottom(self):
208 return self._get_field('bottom.old')
209
210 def get_bottom(self):
211 return self._get_field('bottom')
212
213 def set_bottom(self, value, backup = False):
214 if backup:
215 curr = self._get_field('bottom')
216 self._set_field('bottom.old', curr)
217 self._set_field('bottom', value)
218
219 def get_old_top(self):
220 return self._get_field('top.old')
221
222 def get_top(self):
223 return self._get_field('top')
224
225 def set_top(self, value, backup = False):
226 if backup:
227 curr = self._get_field('top')
228 self._set_field('top.old', curr)
229 self._set_field('top', value)
230 self.__update_top_ref(value)
231
232 def restore_old_boundaries(self):
233 bottom = self._get_field('bottom.old')
234 top = self._get_field('top.old')
235
236 if top and bottom:
237 self._set_field('bottom', bottom)
238 self._set_field('top', top)
239 self.__update_top_ref(top)
240 return True
241 else:
242 return False
243
244 def get_description(self):
245 return self._get_field('description', True)
246
247 def set_description(self, line):
248 self._set_field('description', line, True)
249
250 def get_authname(self):
251 return self._get_field('authname')
252
253 def set_authname(self, name):
254 self._set_field('authname', name or git.author().name)
255
256 def get_authemail(self):
257 return self._get_field('authemail')
258
259 def set_authemail(self, email):
260 self._set_field('authemail', email or git.author().email)
261
262 def get_authdate(self):
263 return self._get_field('authdate')
264
265 def set_authdate(self, date):
266 self._set_field('authdate', date or git.author().date)
267
268 def get_commname(self):
269 return self._get_field('commname')
270
271 def set_commname(self, name):
272 self._set_field('commname', name or git.committer().name)
273
274 def get_commemail(self):
275 return self._get_field('commemail')
276
277 def set_commemail(self, email):
278 self._set_field('commemail', email or git.committer().email)
279
280 def get_log(self):
281 return self._get_field('log')
282
283 def set_log(self, value, backup = False):
284 self._set_field('log', value)
285 self.__update_log_ref(value)
286
287
288 class Series(StgitObject):
289 """Class including the operations on series
290 """
291 def __init__(self, name = None):
292 """Takes a series name as the parameter.
293 """
294 try:
295 if name:
296 self.__name = name
297 else:
298 self.__name = git.get_head_file()
299 self.__base_dir = basedir.get()
300 except git.GitException, ex:
301 raise StackException, 'GIT tree not initialised: %s' % ex
302
303 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
304 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
305 self.__name)
306 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
307 self.__name)
308
309 self.__applied_file = os.path.join(self._dir(), 'applied')
310 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
311 self.__hidden_file = os.path.join(self._dir(), 'hidden')
312 self.__current_file = os.path.join(self._dir(), 'current')
313 self.__descr_file = os.path.join(self._dir(), 'description')
314
315 # where this series keeps its patches
316 self.__patch_dir = os.path.join(self._dir(), 'patches')
317 if not os.path.isdir(self.__patch_dir):
318 self.__patch_dir = self._dir()
319
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
322 os.makedirs(self.__refs_dir)
323 for patch in self.get_applied() + self.get_unapplied():
324 self.get_patch(patch).update_top_ref()
325
326 # trash directory
327 self.__trash_dir = os.path.join(self._dir(), 'trash')
328 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
329 os.makedirs(self.__trash_dir)
330
331 def __patch_name_valid(self, name):
332 """Raise an exception if the patch name is not valid.
333 """
334 if not name or re.search('[^\w.-]', name):
335 raise StackException, 'Invalid patch name: "%s"' % name
336
337 def get_branch(self):
338 """Return the branch name for the Series object
339 """
340 return self.__name
341
342 def __set_current(self, name):
343 """Sets the topmost patch
344 """
345 self._set_field('current', name)
346
347 def get_patch(self, name):
348 """Return a Patch object for the given name
349 """
350 return Patch(name, self.__patch_dir, self.__refs_dir)
351
352 def get_current_patch(self):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt = self.get_current()
356 if not crt:
357 return None
358 return Patch(crt, self.__patch_dir, self.__refs_dir)
359
360 def get_current(self):
361 """Return the name of the topmost patch, or None if there is
362 no such patch."""
363 name = self._get_field('current')
364 if name == '':
365 return None
366 else:
367 return name
368
369 def get_applied(self):
370 if not os.path.isfile(self.__applied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__applied_file)
373 names = [line.strip() for line in f.readlines()]
374 f.close()
375 return names
376
377 def get_unapplied(self):
378 if not os.path.isfile(self.__unapplied_file):
379 raise StackException, 'Branch "%s" not initialised' % self.__name
380 f = file(self.__unapplied_file)
381 names = [line.strip() for line in f.readlines()]
382 f.close()
383 return names
384
385 def get_hidden(self):
386 if not os.path.isfile(self.__hidden_file):
387 return []
388 f = file(self.__hidden_file)
389 names = [line.strip() for line in f.readlines()]
390 f.close()
391 return names
392
393 def get_base_file(self):
394 self.__begin_stack_check()
395 return self.__base_file
396
397 def get_protected(self):
398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
399
400 def protect(self):
401 protect_file = os.path.join(self._dir(), 'protected')
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
404
405 def unprotect(self):
406 protect_file = os.path.join(self._dir(), 'protected')
407 if os.path.isfile(protect_file):
408 os.remove(protect_file)
409
410 def get_description(self):
411 return self._get_field('description') or ''
412
413 def set_description(self, line):
414 self._set_field('description', line)
415
416 def get_parent_remote(self):
417 value = config.get('branch.%s.remote' % self.__name)
418 if value:
419 return value
420 elif 'origin' in git.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", defaulting to "origin".' \
422 'Consider setting "branch.%s.remote" with "git repo-config".' \
423 % (self.__name, self.__name)
424 return 'origin'
425 else:
426 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
427
428 def __set_parent_remote(self, remote):
429 value = config.set('branch.%s.remote' % self.__name, remote)
430
431 def get_parent_branch(self):
432 value = config.get('branch.%s.merge' % self.__name)
433 if value:
434 return value
435 elif git.rev_parse('heads/origin'):
436 print 'Notice: no parent branch declared for stack "%s", defaulting to "heads/origin".' \
437 'Consider setting "branch.%s.merge" with "git repo-config".' \
438 % (self.__name, self.__name)
439 return 'heads/origin'
440 else:
441 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
442
443 def __set_parent_branch(self, name):
444 config.set('branch.%s.merge' % self.__name, name)
445
446 def set_parent(self, remote, localbranch):
447 if localbranch:
448 self.__set_parent_branch(localbranch)
449 if remote:
450 self.__set_parent_remote(remote)
451 elif remote:
452 raise StackException, 'Remote "%s" without a branch cannot be used as parent' % remote
453
454 def __patch_is_current(self, patch):
455 return patch.get_name() == self.get_current()
456
457 def patch_applied(self, name):
458 """Return true if the patch exists in the applied list
459 """
460 return name in self.get_applied()
461
462 def patch_unapplied(self, name):
463 """Return true if the patch exists in the unapplied list
464 """
465 return name in self.get_unapplied()
466
467 def patch_hidden(self, name):
468 """Return true if the patch is hidden.
469 """
470 return name in self.get_hidden()
471
472 def patch_exists(self, name):
473 """Return true if there is a patch with the given name, false
474 otherwise."""
475 return self.patch_applied(name) or self.patch_unapplied(name)
476
477 def __begin_stack_check(self):
478 """Save the current HEAD into .git/refs/heads/base if the stack
479 is empty
480 """
481 if len(self.get_applied()) == 0:
482 head = git.get_head()
483 write_string(self.__base_file, head)
484
485 def __end_stack_check(self):
486 """Remove .git/refs/heads/base if the stack is empty.
487 This warning should never happen
488 """
489 if len(self.get_applied()) == 0 \
490 and read_string(self.__base_file) != git.get_head():
491 print 'Warning: stack empty but the HEAD and base are different'
492
493 def head_top_equal(self):
494 """Return true if the head and the top are the same
495 """
496 crt = self.get_current_patch()
497 if not crt:
498 # we don't care, no patches applied
499 return True
500 return git.get_head() == crt.get_top()
501
502 def is_initialised(self):
503 """Checks if series is already initialised
504 """
505 return os.path.isdir(self.__patch_dir)
506
507 def init(self, create_at=False, parent_remote=None, parent_branch=None):
508 """Initialises the stgit series
509 """
510 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
511
512 if os.path.exists(self.__patch_dir):
513 raise StackException, self.__patch_dir + ' already exists'
514 if os.path.exists(self.__refs_dir):
515 raise StackException, self.__refs_dir + ' already exists'
516 if os.path.exists(self.__base_file):
517 raise StackException, self.__base_file + ' already exists'
518
519 if (create_at!=False):
520 git.create_branch(self.__name, create_at)
521
522 os.makedirs(self.__patch_dir)
523
524 self.set_parent(parent_remote, parent_branch)
525
526 create_dirs(bases_dir)
527
528 self.create_empty_field('applied')
529 self.create_empty_field('unapplied')
530 self.create_empty_field('description')
531 os.makedirs(os.path.join(self._dir(), 'patches'))
532 os.makedirs(self.__refs_dir)
533 self.__begin_stack_check()
534
535 def convert(self):
536 """Either convert to use a separate patch directory, or
537 unconvert to place the patches in the same directory with
538 series control files
539 """
540 if self.__patch_dir == self._dir():
541 print 'Converting old-style to new-style...',
542 sys.stdout.flush()
543
544 self.__patch_dir = os.path.join(self._dir(), 'patches')
545 os.makedirs(self.__patch_dir)
546
547 for p in self.get_applied() + self.get_unapplied():
548 src = os.path.join(self._dir(), p)
549 dest = os.path.join(self.__patch_dir, p)
550 os.rename(src, dest)
551
552 print 'done'
553
554 else:
555 print 'Converting new-style to old-style...',
556 sys.stdout.flush()
557
558 for p in self.get_applied() + self.get_unapplied():
559 src = os.path.join(self.__patch_dir, p)
560 dest = os.path.join(self._dir(), p)
561 os.rename(src, dest)
562
563 if not os.listdir(self.__patch_dir):
564 os.rmdir(self.__patch_dir)
565 print 'done'
566 else:
567 print 'Patch directory %s is not empty.' % self.__name
568
569 self.__patch_dir = self._dir()
570
571 def rename(self, to_name):
572 """Renames a series
573 """
574 to_stack = Series(to_name)
575
576 if to_stack.is_initialised():
577 raise StackException, '"%s" already exists' % to_stack.get_branch()
578 if os.path.exists(to_stack.__base_file):
579 os.remove(to_stack.__base_file)
580
581 git.rename_branch(self.__name, to_name)
582
583 if os.path.isdir(self._dir()):
584 rename(os.path.join(self.__base_dir, 'patches'),
585 self.__name, to_stack.__name)
586 if os.path.exists(self.__base_file):
587 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
588 self.__name, to_stack.__name)
589 if os.path.exists(self.__refs_dir):
590 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
591 self.__name, to_stack.__name)
592
593 self.__init__(to_name)
594
595 def clone(self, target_series):
596 """Clones a series
597 """
598 try:
599 # allow cloning of branches not under StGIT control
600 base = read_string(self.get_base_file())
601 except:
602 base = git.get_head()
603 Series(target_series).init(create_at = base)
604 new_series = Series(target_series)
605
606 # generate an artificial description file
607 new_series.set_description('clone of "%s"' % self.__name)
608
609 # clone self's entire series as unapplied patches
610 try:
611 # allow cloning of branches not under StGIT control
612 applied = self.get_applied()
613 unapplied = self.get_unapplied()
614 patches = applied + unapplied
615 patches.reverse()
616 except:
617 patches = applied = unapplied = []
618 for p in patches:
619 patch = self.get_patch(p)
620 new_series.new_patch(p, message = patch.get_description(),
621 can_edit = False, unapplied = True,
622 bottom = patch.get_bottom(),
623 top = patch.get_top(),
624 author_name = patch.get_authname(),
625 author_email = patch.get_authemail(),
626 author_date = patch.get_authdate())
627
628 # fast forward the cloned series to self's top
629 new_series.forward_patches(applied)
630
631 def delete(self, force = False):
632 """Deletes an stgit series
633 """
634 if self.is_initialised():
635 patches = self.get_unapplied() + self.get_applied()
636 if not force and patches:
637 raise StackException, \
638 'Cannot delete: the series still contains patches'
639 for p in patches:
640 Patch(p, self.__patch_dir, self.__refs_dir).delete()
641
642 # remove the trash directory
643 for fname in os.listdir(self.__trash_dir):
644 os.remove(fname)
645 os.rmdir(self.__trash_dir)
646
647 # FIXME: find a way to get rid of those manual removals
648 # (move functionnality to StgitObject ?)
649 if os.path.exists(self.__applied_file):
650 os.remove(self.__applied_file)
651 if os.path.exists(self.__unapplied_file):
652 os.remove(self.__unapplied_file)
653 if os.path.exists(self.__hidden_file):
654 os.remove(self.__hidden_file)
655 if os.path.exists(self.__current_file):
656 os.remove(self.__current_file)
657 if os.path.exists(self.__descr_file):
658 os.remove(self.__descr_file)
659 if not os.listdir(self.__patch_dir):
660 os.rmdir(self.__patch_dir)
661 else:
662 print 'Patch directory %s is not empty.' % self.__name
663 if not os.listdir(self._dir()):
664 remove_dirs(os.path.join(self.__base_dir, 'patches'),
665 self.__name)
666 else:
667 print 'Series directory %s is not empty.' % self.__name
668 if not os.listdir(self.__refs_dir):
669 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
670 self.__name)
671 else:
672 print 'Refs directory %s is not empty.' % self.__refs_dir
673
674 if os.path.exists(self.__base_file):
675 remove_file_and_dirs(
676 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
677
678 def refresh_patch(self, files = None, message = None, edit = False,
679 show_patch = False,
680 cache_update = True,
681 author_name = None, author_email = None,
682 author_date = None,
683 committer_name = None, committer_email = None,
684 backup = False, sign_str = None, log = 'refresh'):
685 """Generates a new commit for the given patch
686 """
687 name = self.get_current()
688 if not name:
689 raise StackException, 'No patches applied'
690
691 patch = Patch(name, self.__patch_dir, self.__refs_dir)
692
693 descr = patch.get_description()
694 if not (message or descr):
695 edit = True
696 descr = ''
697 elif message:
698 descr = message
699
700 if not message and edit:
701 descr = edit_file(self, descr.rstrip(), \
702 'Please edit the description for patch "%s" ' \
703 'above.' % name, show_patch)
704
705 if not author_name:
706 author_name = patch.get_authname()
707 if not author_email:
708 author_email = patch.get_authemail()
709 if not author_date:
710 author_date = patch.get_authdate()
711 if not committer_name:
712 committer_name = patch.get_commname()
713 if not committer_email:
714 committer_email = patch.get_commemail()
715
716 if sign_str:
717 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
718 committer_name, committer_email)
719
720 bottom = patch.get_bottom()
721
722 commit_id = git.commit(files = files,
723 message = descr, parents = [bottom],
724 cache_update = cache_update,
725 allowempty = True,
726 author_name = author_name,
727 author_email = author_email,
728 author_date = author_date,
729 committer_name = committer_name,
730 committer_email = committer_email)
731
732 patch.set_bottom(bottom, backup = backup)
733 patch.set_top(commit_id, backup = backup)
734 patch.set_description(descr)
735 patch.set_authname(author_name)
736 patch.set_authemail(author_email)
737 patch.set_authdate(author_date)
738 patch.set_commname(committer_name)
739 patch.set_commemail(committer_email)
740
741 if log:
742 self.log_patch(patch, log)
743
744 return commit_id
745
746 def undo_refresh(self):
747 """Undo the patch boundaries changes caused by 'refresh'
748 """
749 name = self.get_current()
750 assert(name)
751
752 patch = Patch(name, self.__patch_dir, self.__refs_dir)
753 old_bottom = patch.get_old_bottom()
754 old_top = patch.get_old_top()
755
756 # the bottom of the patch is not changed by refresh. If the
757 # old_bottom is different, there wasn't any previous 'refresh'
758 # command (probably only a 'push')
759 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
760 raise StackException, 'No undo information available'
761
762 git.reset(tree_id = old_top, check_out = False)
763 if patch.restore_old_boundaries():
764 self.log_patch(patch, 'undo')
765
766 def new_patch(self, name, message = None, can_edit = True,
767 unapplied = False, show_patch = False,
768 top = None, bottom = None,
769 author_name = None, author_email = None, author_date = None,
770 committer_name = None, committer_email = None,
771 before_existing = False, refresh = True):
772 """Creates a new patch
773 """
774 self.__patch_name_valid(name)
775
776 if self.patch_applied(name) or self.patch_unapplied(name):
777 raise StackException, 'Patch "%s" already exists' % name
778
779 if not message and can_edit:
780 descr = edit_file(self, None, \
781 'Please enter the description for patch "%s" ' \
782 'above.' % name, show_patch)
783 else:
784 descr = message
785
786 head = git.get_head()
787
788 self.__begin_stack_check()
789
790 patch = Patch(name, self.__patch_dir, self.__refs_dir)
791 patch.create()
792
793 if bottom:
794 patch.set_bottom(bottom)
795 else:
796 patch.set_bottom(head)
797 if top:
798 patch.set_top(top)
799 else:
800 patch.set_top(head)
801
802 patch.set_description(descr)
803 patch.set_authname(author_name)
804 patch.set_authemail(author_email)
805 patch.set_authdate(author_date)
806 patch.set_commname(committer_name)
807 patch.set_commemail(committer_email)
808
809 if unapplied:
810 self.log_patch(patch, 'new')
811
812 patches = [patch.get_name()] + self.get_unapplied()
813
814 f = file(self.__unapplied_file, 'w+')
815 f.writelines([line + '\n' for line in patches])
816 f.close()
817 elif before_existing:
818 self.log_patch(patch, 'new')
819
820 insert_string(self.__applied_file, patch.get_name())
821 if not self.get_current():
822 self.__set_current(name)
823 else:
824 append_string(self.__applied_file, patch.get_name())
825 self.__set_current(name)
826 if refresh:
827 self.refresh_patch(cache_update = False, log = 'new')
828
829 def delete_patch(self, name):
830 """Deletes a patch
831 """
832 self.__patch_name_valid(name)
833 patch = Patch(name, self.__patch_dir, self.__refs_dir)
834
835 if self.__patch_is_current(patch):
836 self.pop_patch(name)
837 elif self.patch_applied(name):
838 raise StackException, 'Cannot remove an applied patch, "%s", ' \
839 'which is not current' % name
840 elif not name in self.get_unapplied():
841 raise StackException, 'Unknown patch "%s"' % name
842
843 # save the commit id to a trash file
844 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
845
846 patch.delete()
847
848 unapplied = self.get_unapplied()
849 unapplied.remove(name)
850 f = file(self.__unapplied_file, 'w+')
851 f.writelines([line + '\n' for line in unapplied])
852 f.close()
853
854 if self.patch_hidden(name):
855 self.unhide_patch(name)
856
857 self.__begin_stack_check()
858
859 def forward_patches(self, names):
860 """Try to fast-forward an array of patches.
861
862 On return, patches in names[0:returned_value] have been pushed on the
863 stack. Apply the rest with push_patch
864 """
865 unapplied = self.get_unapplied()
866 self.__begin_stack_check()
867
868 forwarded = 0
869 top = git.get_head()
870
871 for name in names:
872 assert(name in unapplied)
873
874 patch = Patch(name, self.__patch_dir, self.__refs_dir)
875
876 head = top
877 bottom = patch.get_bottom()
878 top = patch.get_top()
879
880 # top != bottom always since we have a commit for each patch
881 if head == bottom:
882 # reset the backup information. No logging since the
883 # patch hasn't changed
884 patch.set_bottom(head, backup = True)
885 patch.set_top(top, backup = True)
886
887 else:
888 head_tree = git.get_commit(head).get_tree()
889 bottom_tree = git.get_commit(bottom).get_tree()
890 if head_tree == bottom_tree:
891 # We must just reparent this patch and create a new commit
892 # for it
893 descr = patch.get_description()
894 author_name = patch.get_authname()
895 author_email = patch.get_authemail()
896 author_date = patch.get_authdate()
897 committer_name = patch.get_commname()
898 committer_email = patch.get_commemail()
899
900 top_tree = git.get_commit(top).get_tree()
901
902 top = git.commit(message = descr, parents = [head],
903 cache_update = False,
904 tree_id = top_tree,
905 allowempty = True,
906 author_name = author_name,
907 author_email = author_email,
908 author_date = author_date,
909 committer_name = committer_name,
910 committer_email = committer_email)
911
912 patch.set_bottom(head, backup = True)
913 patch.set_top(top, backup = True)
914
915 self.log_patch(patch, 'push(f)')
916 else:
917 top = head
918 # stop the fast-forwarding, must do a real merge
919 break
920
921 forwarded+=1
922 unapplied.remove(name)
923
924 if forwarded == 0:
925 return 0
926
927 git.switch(top)
928
929 append_strings(self.__applied_file, names[0:forwarded])
930
931 f = file(self.__unapplied_file, 'w+')
932 f.writelines([line + '\n' for line in unapplied])
933 f.close()
934
935 self.__set_current(name)
936
937 return forwarded
938
939 def merged_patches(self, names):
940 """Test which patches were merged upstream by reverse-applying
941 them in reverse order. The function returns the list of
942 patches detected to have been applied. The state of the tree
943 is restored to the original one
944 """
945 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
946 for name in names]
947 patches.reverse()
948
949 merged = []
950 for p in patches:
951 if git.apply_diff(p.get_top(), p.get_bottom()):
952 merged.append(p.get_name())
953 merged.reverse()
954
955 git.reset()
956
957 return merged
958
959 def push_patch(self, name, empty = False):
960 """Pushes a patch on the stack
961 """
962 unapplied = self.get_unapplied()
963 assert(name in unapplied)
964
965 self.__begin_stack_check()
966
967 patch = Patch(name, self.__patch_dir, self.__refs_dir)
968
969 head = git.get_head()
970 bottom = patch.get_bottom()
971 top = patch.get_top()
972
973 ex = None
974 modified = False
975
976 # top != bottom always since we have a commit for each patch
977 if empty:
978 # just make an empty patch (top = bottom = HEAD). This
979 # option is useful to allow undoing already merged
980 # patches. The top is updated by refresh_patch since we
981 # need an empty commit
982 patch.set_bottom(head, backup = True)
983 patch.set_top(head, backup = True)
984 modified = True
985 elif head == bottom:
986 # reset the backup information. No need for logging
987 patch.set_bottom(bottom, backup = True)
988 patch.set_top(top, backup = True)
989
990 git.switch(top)
991 else:
992 # new patch needs to be refreshed.
993 # The current patch is empty after merge.
994 patch.set_bottom(head, backup = True)
995 patch.set_top(head, backup = True)
996
997 # Try the fast applying first. If this fails, fall back to the
998 # three-way merge
999 if not git.apply_diff(bottom, top):
1000 # if git.apply_diff() fails, the patch requires a diff3
1001 # merge and can be reported as modified
1002 modified = True
1003
1004 # merge can fail but the patch needs to be pushed
1005 try:
1006 git.merge(bottom, head, top, recursive = True)
1007 except git.GitException, ex:
1008 print >> sys.stderr, \
1009 'The merge failed during "push". ' \
1010 'Use "refresh" after fixing the conflicts'
1011
1012 append_string(self.__applied_file, name)
1013
1014 unapplied.remove(name)
1015 f = file(self.__unapplied_file, 'w+')
1016 f.writelines([line + '\n' for line in unapplied])
1017 f.close()
1018
1019 self.__set_current(name)
1020
1021 # head == bottom case doesn't need to refresh the patch
1022 if empty or head != bottom:
1023 if not ex:
1024 # if the merge was OK and no conflicts, just refresh the patch
1025 # The GIT cache was already updated by the merge operation
1026 if modified:
1027 log = 'push(m)'
1028 else:
1029 log = 'push'
1030 self.refresh_patch(cache_update = False, log = log)
1031 else:
1032 # we store the correctly merged files only for
1033 # tracking the conflict history. Note that the
1034 # git.merge() operations shouls always leave the index
1035 # in a valid state (i.e. only stage 0 files)
1036 self.refresh_patch(cache_update = False, log = 'push(c)')
1037 raise StackException, str(ex)
1038
1039 return modified
1040
1041 def undo_push(self):
1042 name = self.get_current()
1043 assert(name)
1044
1045 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1046 old_bottom = patch.get_old_bottom()
1047 old_top = patch.get_old_top()
1048
1049 # the top of the patch is changed by a push operation only
1050 # together with the bottom (otherwise the top was probably
1051 # modified by 'refresh'). If they are both unchanged, there
1052 # was a fast forward
1053 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1054 raise StackException, 'No undo information available'
1055
1056 git.reset()
1057 self.pop_patch(name)
1058 ret = patch.restore_old_boundaries()
1059 if ret:
1060 self.log_patch(patch, 'undo')
1061
1062 return ret
1063
1064 def pop_patch(self, name, keep = False):
1065 """Pops the top patch from the stack
1066 """
1067 applied = self.get_applied()
1068 applied.reverse()
1069 assert(name in applied)
1070
1071 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1072
1073 # only keep the local changes
1074 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1075 raise StackException, \
1076 'Failed to pop patches while preserving the local changes'
1077
1078 git.switch(patch.get_bottom(), keep)
1079
1080 # save the new applied list
1081 idx = applied.index(name) + 1
1082
1083 popped = applied[:idx]
1084 popped.reverse()
1085 unapplied = popped + self.get_unapplied()
1086
1087 f = file(self.__unapplied_file, 'w+')
1088 f.writelines([line + '\n' for line in unapplied])
1089 f.close()
1090
1091 del applied[:idx]
1092 applied.reverse()
1093
1094 f = file(self.__applied_file, 'w+')
1095 f.writelines([line + '\n' for line in applied])
1096 f.close()
1097
1098 if applied == []:
1099 self.__set_current(None)
1100 else:
1101 self.__set_current(applied[-1])
1102
1103 self.__end_stack_check()
1104
1105 def empty_patch(self, name):
1106 """Returns True if the patch is empty
1107 """
1108 self.__patch_name_valid(name)
1109 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1110 bottom = patch.get_bottom()
1111 top = patch.get_top()
1112
1113 if bottom == top:
1114 return True
1115 elif git.get_commit(top).get_tree() \
1116 == git.get_commit(bottom).get_tree():
1117 return True
1118
1119 return False
1120
1121 def rename_patch(self, oldname, newname):
1122 self.__patch_name_valid(newname)
1123
1124 applied = self.get_applied()
1125 unapplied = self.get_unapplied()
1126
1127 if oldname == newname:
1128 raise StackException, '"To" name and "from" name are the same'
1129
1130 if newname in applied or newname in unapplied:
1131 raise StackException, 'Patch "%s" already exists' % newname
1132
1133 if self.patch_hidden(oldname):
1134 self.unhide_patch(oldname)
1135 self.hide_patch(newname)
1136
1137 if oldname in unapplied:
1138 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1139 unapplied[unapplied.index(oldname)] = newname
1140
1141 f = file(self.__unapplied_file, 'w+')
1142 f.writelines([line + '\n' for line in unapplied])
1143 f.close()
1144 elif oldname in applied:
1145 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1146 if oldname == self.get_current():
1147 self.__set_current(newname)
1148
1149 applied[applied.index(oldname)] = newname
1150
1151 f = file(self.__applied_file, 'w+')
1152 f.writelines([line + '\n' for line in applied])
1153 f.close()
1154 else:
1155 raise StackException, 'Unknown patch "%s"' % oldname
1156
1157 def log_patch(self, patch, message):
1158 """Generate a log commit for a patch
1159 """
1160 top = git.get_commit(patch.get_top())
1161 msg = '%s\t%s' % (message, top.get_id_hash())
1162
1163 old_log = patch.get_log()
1164 if old_log:
1165 parents = [old_log]
1166 else:
1167 parents = []
1168
1169 log = git.commit(message = msg, parents = parents,
1170 cache_update = False, tree_id = top.get_tree(),
1171 allowempty = True)
1172 patch.set_log(log)
1173
1174 def hide_patch(self, name):
1175 """Add the patch to the hidden list.
1176 """
1177 if not self.patch_exists(name):
1178 raise StackException, 'Unknown patch "%s"' % name
1179 elif self.patch_hidden(name):
1180 raise StackException, 'Patch "%s" already hidden' % name
1181
1182 append_string(self.__hidden_file, name)
1183
1184 def unhide_patch(self, name):
1185 """Add the patch to the hidden list.
1186 """
1187 if not self.patch_exists(name):
1188 raise StackException, 'Unknown patch "%s"' % name
1189 hidden = self.get_hidden()
1190 if not name in hidden:
1191 raise StackException, 'Patch "%s" not hidden' % name
1192
1193 hidden.remove(name)
1194
1195 f = file(self.__hidden_file, 'w+')
1196 f.writelines([line + '\n' for line in hidden])
1197 f.close()