publish: Add '--last' option to show the last published patch
[stgit] / stgit / commands / publish.py
CommitLineData
e58f264a
CM
1__copyright__ = """
2Copyright (C) 2009, Catalin Marinas <catalin.marinas@gmail.com>
3
4This program is free software; you can redistribute it and/or modify
5it under the terms of the GNU General Public License version 2 as
6published by the Free Software Foundation.
7
8This program is distributed in the hope that it will be useful,
9but WITHOUT ANY WARRANTY; without even the implied warranty of
10MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11GNU General Public License for more details.
12
13You should have received a copy of the GNU General Public License
14along with this program; if not, write to the Free Software
15Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16"""
17
18from stgit import argparse
19from stgit.argparse import opt
20from stgit.commands import common
21from stgit.config import config
eec51c11 22from stgit.lib import git, stack, transaction
e58f264a 23from stgit.out import out
df6e6d0c 24from stgit import utils
e58f264a 25
737b7d8e 26help = 'Push the stack changes to a merge-friendly branch'
e58f264a 27kind = 'stack'
54239abf 28usage = ['[options] [--] [branch]']
e58f264a
CM
29description = """
30This command commits a set of changes on a separate (called public) branch
31based on the modifications of the given or current stack. The history of the
32public branch is not re-written, making it merge-friendly and feasible for
33publishing. The heads of the stack and public branch may be different but the
34corresponding tree objects are always the same.
35
36If the trees of the stack and public branch are different (otherwise the
37command has no effect), StGit first checks for a rebase of the stack since the
38last publishing. If a rebase is detected, StGit creates a commit on the public
39branch corresponding to a merge between the new stack base and the latest
40public head.
41
42If no rebasing was detected, StGit checks for new patches that may have been
43created on top of the stack since the last publishing. If new patches are
44found and are not empty, they are checked into the public branch keeping the
45same commit information (e.g. log message, author, committer, date).
46
47If the above tests fail (e.g. patches modified or removed), StGit creates a
48new commit on the public branch having the same tree as the stack but the
49public head as its parent. The editor will be invoked if no "--message" option
50is given.
51
52It is recommended that stack modifications falling in different categories as
53described above are separated by a publish command in order to keep the public
54branch history cleaner (otherwise StGit would generate a big commit including
55several stack modifications).
56
eec51c11
CM
57The '--unpublished' option can be used to check if there are applied patches
58that have not been published to the public branch. This is done by trying to
59revert the patches in the public tree (similar to the 'push --merged'
064161c3
CM
60detection). The '--last' option tries to find the last published patch by
61checking the SHA1 of the patch tree agains the public tree. This may fail if
62the stack was rebased since the last publish command.
eec51c11 63
e58f264a
CM
64The public branch name can be set via the branch.<branch>.public configuration
65variable (defaulting to "<branch>.public").
66"""
67
68args = [argparse.all_branches]
69options = [
70 opt('-b', '--branch', args = [argparse.stg_branches],
eec51c11 71 short = 'Use BRANCH instead of the default branch'),
064161c3
CM
72 opt('-l', '--last', action = 'store_true',
73 short = 'Show the last published patch'),
eec51c11
CM
74 opt('-u', '--unpublished', action = 'store_true',
75 short = 'Show applied patches that have not been published')
e58f264a
CM
76] + (argparse.author_options()
77 + argparse.message_options(save_template = False)
78 + argparse.sign_options())
79
80directory = common.DirectoryHasRepositoryLib()
81
82def __create_commit(repository, tree, parents, options, message = ''):
83 """Return a new Commit object."""
84 cd = git.CommitData(
85 tree = tree, parents = parents, message = message,
86 author = git.Person.author(), committer = git.Person.committer())
af734f49 87 cd = common.update_commit_data(cd, options)
e58f264a
CM
88
89 return repository.commit(cd)
90
eec51c11
CM
91def __get_published(stack, tree):
92 """Check the patches that were already published."""
93 trans = transaction.StackTransaction(stack, 'publish')
94 published = trans.check_merged(trans.applied, tree = tree, quiet = True)
95 trans.abort()
96
97 return published
98
064161c3
CM
99def __get_last(stack, tree):
100 """Return the name of the last published patch."""
101 for p in reversed(stack.patchorder.applied):
102 pc = stack.patches.get(p).commit
103 if tree.sha1 == pc.data.tree.sha1:
104 return p
105
106 return None
107
e58f264a
CM
108def func(parser, options, args):
109 """Publish the stack changes."""
110 repository = directory.repository
111 stack = repository.get_stack(options.branch)
112
113 if not args:
5660771c 114 public_ref = common.get_public_ref(stack.name)
e58f264a
CM
115 elif len(args) == 1:
116 public_ref = args[0]
117 else:
118 parser.error('incorrect number of arguments')
119
df6e6d0c
CM
120 if not public_ref.startswith('refs/heads/'):
121 public_ref = 'refs/heads/' + public_ref
122
e58f264a
CM
123 # just clone the stack if the public ref does not exist
124 if not repository.refs.exists(public_ref):
064161c3 125 if options.unpublished or options.last:
eec51c11 126 raise common.CmdException('"%s" does not exist' % public_ref)
e58f264a
CM
127 repository.refs.set(public_ref, stack.head, 'publish')
128 out.info('Created "%s"' % public_ref)
129 return
130
131 public_head = repository.refs.get(public_ref)
132 public_tree = public_head.data.tree
133
064161c3
CM
134 # find the last published patch
135 if options.last:
136 last = __get_last(stack, public_tree)
137 if not last:
138 raise common.CmdException('Unable to find the last published patch '
139 '(possibly rebased stack)')
140 out.info('%s' % last)
141 return
142
e58f264a
CM
143 # check for same tree (already up to date)
144 if public_tree.sha1 == stack.head.data.tree.sha1:
145 out.info('"%s" already up to date' % public_ref)
146 return
147
eec51c11
CM
148 # check for unpublished patches
149 if options.unpublished:
150 published = set(__get_published(stack, public_tree))
151 for p in stack.patchorder.applied:
152 if p not in published:
153 print p
154 return
155
e58f264a
CM
156 # check for rebased stack. In this case we emulate a merge with the stack
157 # base by setting two parents.
158 merge_bases = repository.get_merge_bases(public_head, stack.base)
6c052b02
CM
159 if public_head in merge_bases:
160 # fast-forward the public ref
161 repository.refs.set(public_ref, stack.head, 'publish')
162 out.info('Fast-forwarded "%s"' % public_ref)
163 return
e58f264a 164 if not stack.base in merge_bases:
9a4a9c2c 165 message = 'Merge %s into %s' % (repository.describe(stack.base).strip(),
df6e6d0c
CM
166 utils.strip_prefix('refs/heads/',
167 public_ref))
e58f264a
CM
168 public_head = __create_commit(repository, stack.head.data.tree,
169 [public_head, stack.base], options,
170 message)
171 repository.refs.set(public_ref, public_head, 'publish')
172 out.info('Merged the stack base into "%s"' % public_ref)
173 return
174
175 # check for new patches from the last publishing. This is done by checking
176 # whether the public tree is the same as the bottom of the checked patch.
177 # If older patches were modified, new patches cannot be detected. The new
178 # patches and their metadata are pushed directly to the published head.
179 for p in stack.patchorder.applied:
180 pc = stack.patches.get(p).commit
181 if public_tree.sha1 == pc.data.parent.data.tree.sha1:
182 if pc.data.is_nochange():
183 out.info('Ignored new empty patch "%s"' % p)
184 continue
185 cd = pc.data.set_parent(public_head)
186 public_head = repository.commit(cd)
187 public_tree = public_head.data.tree
eb0c552f 188 out.info('Published new patch "%s"' % p)
e58f264a
CM
189
190 # create a new commit (only happens if no new patches are detected)
191 if public_tree.sha1 != stack.head.data.tree.sha1:
192 public_head = __create_commit(repository, stack.head.data.tree,
193 [public_head], options)
194
195 # update the public head
196 repository.refs.set(public_ref, public_head, 'publish')
197 out.info('Updated "%s"' % public_ref)