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