##// END OF EJS Templates
Add support for diffstat in commit emails, and move diffstat from...
Matt Doar -
r3096:f422c826 default
parent child Browse files
Show More
@@ -1,276 +1,279 b''
1 # notify.py - email notifications for mercurial
1 # notify.py - email notifications for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 #
7 #
8 # hook extension to email notifications to people when changesets are
8 # hook extension to email notifications to people when changesets are
9 # committed to a repo they subscribe to.
9 # committed to a repo they subscribe to.
10 #
10 #
11 # default mode is to print messages to stdout, for testing and
11 # default mode is to print messages to stdout, for testing and
12 # configuring.
12 # configuring.
13 #
13 #
14 # to use, configure notify extension and enable in hgrc like this:
14 # to use, configure notify extension and enable in hgrc like this:
15 #
15 #
16 # [extensions]
16 # [extensions]
17 # hgext.notify =
17 # hgext.notify =
18 #
18 #
19 # [hooks]
19 # [hooks]
20 # # one email for each incoming changeset
20 # # one email for each incoming changeset
21 # incoming.notify = python:hgext.notify.hook
21 # incoming.notify = python:hgext.notify.hook
22 # # batch emails when many changesets incoming at one time
22 # # batch emails when many changesets incoming at one time
23 # changegroup.notify = python:hgext.notify.hook
23 # changegroup.notify = python:hgext.notify.hook
24 #
24 #
25 # [notify]
25 # [notify]
26 # # config items go in here
26 # # config items go in here
27 #
27 #
28 # config items:
28 # config items:
29 #
29 #
30 # REQUIRED:
30 # REQUIRED:
31 # config = /path/to/file # file containing subscriptions
31 # config = /path/to/file # file containing subscriptions
32 #
32 #
33 # OPTIONAL:
33 # OPTIONAL:
34 # test = True # print messages to stdout for testing
34 # test = True # print messages to stdout for testing
35 # strip = 3 # number of slashes to strip for url paths
35 # strip = 3 # number of slashes to strip for url paths
36 # domain = example.com # domain to use if committer missing domain
36 # domain = example.com # domain to use if committer missing domain
37 # style = ... # style file to use when formatting email
37 # style = ... # style file to use when formatting email
38 # template = ... # template to use when formatting email
38 # template = ... # template to use when formatting email
39 # incoming = ... # template to use when run as incoming hook
39 # incoming = ... # template to use when run as incoming hook
40 # changegroup = ... # template when run as changegroup hook
40 # changegroup = ... # template when run as changegroup hook
41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 # maxsubject = 67 # truncate subject line longer than this
42 # maxsubject = 67 # truncate subject line longer than this
43 # sources = serve # notify if source of incoming changes in this list
43 # sources = serve # notify if source of incoming changes in this list
44 # # (serve == ssh or http, push, pull, bundle)
44 # # (serve == ssh or http, push, pull, bundle)
45 # [email]
45 # [email]
46 # from = user@host.com # email address to send as if none given
46 # from = user@host.com # email address to send as if none given
47 # [web]
47 # [web]
48 # baseurl = http://hgserver/... # root of hg web site for browsing commits
48 # baseurl = http://hgserver/... # root of hg web site for browsing commits
49 #
49 #
50 # notify config file has same format as regular hgrc. it has two
50 # notify config file has same format as regular hgrc. it has two
51 # sections so you can express subscriptions in whatever way is handier
51 # sections so you can express subscriptions in whatever way is handier
52 # for you.
52 # for you.
53 #
53 #
54 # [usersubs]
54 # [usersubs]
55 # # key is subscriber email, value is ","-separated list of glob patterns
55 # # key is subscriber email, value is ","-separated list of glob patterns
56 # user@host = pattern
56 # user@host = pattern
57 #
57 #
58 # [reposubs]
58 # [reposubs]
59 # # key is glob pattern, value is ","-separated list of subscriber emails
59 # # key is glob pattern, value is ","-separated list of subscriber emails
60 # pattern = user@host
60 # pattern = user@host
61 #
61 #
62 # glob patterns are matched against path to repo root.
62 # glob patterns are matched against path to repo root.
63 #
63 #
64 # if you like, you can put notify config file in repo that users can
64 # if you like, you can put notify config file in repo that users can
65 # push changes to, they can manage their own subscriptions.
65 # push changes to, they can manage their own subscriptions.
66
66
67 from mercurial.demandload import *
67 from mercurial.demandload import *
68 from mercurial.i18n import gettext as _
68 from mercurial.i18n import gettext as _
69 from mercurial.node import *
69 from mercurial.node import *
70 demandload(globals(), 'mercurial:commands,patch,templater,util,mail')
70 demandload(globals(), 'mercurial:commands,patch,templater,util,mail')
71 demandload(globals(), 'email.Parser fnmatch socket time')
71 demandload(globals(), 'email.Parser fnmatch socket time')
72
72
73 # template for single changeset can include email headers.
73 # template for single changeset can include email headers.
74 single_template = '''
74 single_template = '''
75 Subject: changeset in {webroot}: {desc|firstline|strip}
75 Subject: changeset in {webroot}: {desc|firstline|strip}
76 From: {author}
76 From: {author}
77
77
78 changeset {node|short} in {root}
78 changeset {node|short} in {root}
79 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
79 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
80 description:
80 description:
81 \t{desc|tabindent|strip}
81 \t{desc|tabindent|strip}
82 '''.lstrip()
82 '''.lstrip()
83
83
84 # template for multiple changesets should not contain email headers,
84 # template for multiple changesets should not contain email headers,
85 # because only first set of headers will be used and result will look
85 # because only first set of headers will be used and result will look
86 # strange.
86 # strange.
87 multiple_template = '''
87 multiple_template = '''
88 changeset {node|short} in {root}
88 changeset {node|short} in {root}
89 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
89 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
90 summary: {desc|firstline}
90 summary: {desc|firstline}
91 '''
91 '''
92
92
93 deftemplates = {
93 deftemplates = {
94 'changegroup': multiple_template,
94 'changegroup': multiple_template,
95 }
95 }
96
96
97 class notifier(object):
97 class notifier(object):
98 '''email notification class.'''
98 '''email notification class.'''
99
99
100 def __init__(self, ui, repo, hooktype):
100 def __init__(self, ui, repo, hooktype):
101 self.ui = ui
101 self.ui = ui
102 cfg = self.ui.config('notify', 'config')
102 cfg = self.ui.config('notify', 'config')
103 if cfg:
103 if cfg:
104 self.ui.readconfig(cfg)
104 self.ui.readconfig(cfg)
105 self.repo = repo
105 self.repo = repo
106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
107 self.root = self.strip(self.repo.root)
107 self.root = self.strip(self.repo.root)
108 self.domain = self.ui.config('notify', 'domain')
108 self.domain = self.ui.config('notify', 'domain')
109 self.sio = templater.stringio()
109 self.sio = templater.stringio()
110 self.subs = self.subscribers()
110 self.subs = self.subscribers()
111
111
112 mapfile = self.ui.config('notify', 'style')
112 mapfile = self.ui.config('notify', 'style')
113 template = (self.ui.config('notify', hooktype) or
113 template = (self.ui.config('notify', hooktype) or
114 self.ui.config('notify', 'template'))
114 self.ui.config('notify', 'template'))
115 self.t = templater.changeset_templater(self.ui, self.repo, mapfile,
115 self.t = templater.changeset_templater(self.ui, self.repo, mapfile,
116 self.sio)
116 self.sio)
117 if not mapfile and not template:
117 if not mapfile and not template:
118 template = deftemplates.get(hooktype) or single_template
118 template = deftemplates.get(hooktype) or single_template
119 if template:
119 if template:
120 template = templater.parsestring(template, quoted=False)
120 template = templater.parsestring(template, quoted=False)
121 self.t.use_template(template)
121 self.t.use_template(template)
122
122
123 def strip(self, path):
123 def strip(self, path):
124 '''strip leading slashes from local path, turn into web-safe path.'''
124 '''strip leading slashes from local path, turn into web-safe path.'''
125
125
126 path = util.pconvert(path)
126 path = util.pconvert(path)
127 count = self.stripcount
127 count = self.stripcount
128 while count > 0:
128 while count > 0:
129 c = path.find('/')
129 c = path.find('/')
130 if c == -1:
130 if c == -1:
131 break
131 break
132 path = path[c+1:]
132 path = path[c+1:]
133 count -= 1
133 count -= 1
134 return path
134 return path
135
135
136 def fixmail(self, addr):
136 def fixmail(self, addr):
137 '''try to clean up email addresses.'''
137 '''try to clean up email addresses.'''
138
138
139 addr = templater.email(addr.strip())
139 addr = templater.email(addr.strip())
140 a = addr.find('@localhost')
140 a = addr.find('@localhost')
141 if a != -1:
141 if a != -1:
142 addr = addr[:a]
142 addr = addr[:a]
143 if '@' not in addr:
143 if '@' not in addr:
144 return addr + '@' + self.domain
144 return addr + '@' + self.domain
145 return addr
145 return addr
146
146
147 def subscribers(self):
147 def subscribers(self):
148 '''return list of email addresses of subscribers to this repo.'''
148 '''return list of email addresses of subscribers to this repo.'''
149
149
150 subs = {}
150 subs = {}
151 for user, pats in self.ui.configitems('usersubs'):
151 for user, pats in self.ui.configitems('usersubs'):
152 for pat in pats.split(','):
152 for pat in pats.split(','):
153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
154 subs[self.fixmail(user)] = 1
154 subs[self.fixmail(user)] = 1
155 for pat, users in self.ui.configitems('reposubs'):
155 for pat, users in self.ui.configitems('reposubs'):
156 if fnmatch.fnmatch(self.repo.root, pat):
156 if fnmatch.fnmatch(self.repo.root, pat):
157 for user in users.split(','):
157 for user in users.split(','):
158 subs[self.fixmail(user)] = 1
158 subs[self.fixmail(user)] = 1
159 subs = subs.keys()
159 subs = subs.keys()
160 subs.sort()
160 subs.sort()
161 return subs
161 return subs
162
162
163 def url(self, path=None):
163 def url(self, path=None):
164 return self.ui.config('web', 'baseurl') + (path or self.root)
164 return self.ui.config('web', 'baseurl') + (path or self.root)
165
165
166 def node(self, node):
166 def node(self, node):
167 '''format one changeset.'''
167 '''format one changeset.'''
168
168
169 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
169 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
170 baseurl=self.ui.config('web', 'baseurl'),
170 baseurl=self.ui.config('web', 'baseurl'),
171 root=self.repo.root,
171 root=self.repo.root,
172 webroot=self.root)
172 webroot=self.root)
173
173
174 def skipsource(self, source):
174 def skipsource(self, source):
175 '''true if incoming changes from this source should be skipped.'''
175 '''true if incoming changes from this source should be skipped.'''
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
177 return source not in ok_sources
177 return source not in ok_sources
178
178
179 def send(self, node, count):
179 def send(self, node, count):
180 '''send message.'''
180 '''send message.'''
181
181
182 p = email.Parser.Parser()
182 p = email.Parser.Parser()
183 self.sio.seek(0)
183 self.sio.seek(0)
184 msg = p.parse(self.sio)
184 msg = p.parse(self.sio)
185
185
186 def fix_subject():
186 def fix_subject():
187 '''try to make subject line exist and be useful.'''
187 '''try to make subject line exist and be useful.'''
188
188
189 subject = msg['Subject']
189 subject = msg['Subject']
190 if not subject:
190 if not subject:
191 if count > 1:
191 if count > 1:
192 subject = _('%s: %d new changesets') % (self.root, count)
192 subject = _('%s: %d new changesets') % (self.root, count)
193 else:
193 else:
194 changes = self.repo.changelog.read(node)
194 changes = self.repo.changelog.read(node)
195 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
195 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
196 subject = '%s: %s' % (self.root, s)
196 subject = '%s: %s' % (self.root, s)
197 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
197 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
198 if maxsubject and len(subject) > maxsubject:
198 if maxsubject and len(subject) > maxsubject:
199 subject = subject[:maxsubject-3] + '...'
199 subject = subject[:maxsubject-3] + '...'
200 del msg['Subject']
200 del msg['Subject']
201 msg['Subject'] = subject
201 msg['Subject'] = subject
202
202
203 def fix_sender():
203 def fix_sender():
204 '''try to make message have proper sender.'''
204 '''try to make message have proper sender.'''
205
205
206 sender = msg['From']
206 sender = msg['From']
207 if not sender:
207 if not sender:
208 sender = self.ui.config('email', 'from') or self.ui.username()
208 sender = self.ui.config('email', 'from') or self.ui.username()
209 if '@' not in sender or '@localhost' in sender:
209 if '@' not in sender or '@localhost' in sender:
210 sender = self.fixmail(sender)
210 sender = self.fixmail(sender)
211 del msg['From']
211 del msg['From']
212 msg['From'] = sender
212 msg['From'] = sender
213
213
214 fix_subject()
214 fix_subject()
215 fix_sender()
215 fix_sender()
216
216
217 msg['X-Hg-Notification'] = 'changeset ' + short(node)
217 msg['X-Hg-Notification'] = 'changeset ' + short(node)
218 if not msg['Message-Id']:
218 if not msg['Message-Id']:
219 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
219 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
220 (short(node), int(time.time()),
220 (short(node), int(time.time()),
221 hash(self.repo.root), socket.getfqdn()))
221 hash(self.repo.root), socket.getfqdn()))
222 msg['To'] = ', '.join(self.subs)
222 msg['To'] = ', '.join(self.subs)
223
223
224 msgtext = msg.as_string(0)
224 msgtext = msg.as_string(0)
225 if self.ui.configbool('notify', 'test', True):
225 if self.ui.configbool('notify', 'test', True):
226 self.ui.write(msgtext)
226 self.ui.write(msgtext)
227 if not msgtext.endswith('\n'):
227 if not msgtext.endswith('\n'):
228 self.ui.write('\n')
228 self.ui.write('\n')
229 else:
229 else:
230 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
230 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
231 (len(self.subs), count))
231 (len(self.subs), count))
232 mail.sendmail(self.ui, templater.email(msg['From']),
232 mail.sendmail(self.ui, templater.email(msg['From']),
233 self.subs, msgtext)
233 self.subs, msgtext)
234
234
235 def diff(self, node, ref):
235 def diff(self, node, ref):
236 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
236 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
237 if maxdiff == 0:
237 if maxdiff == 0:
238 return
238 return
239 fp = templater.stringio()
239 fp = templater.stringio()
240 prev = self.repo.changelog.parents(node)[0]
240 prev = self.repo.changelog.parents(node)[0]
241 patch.diff(self.repo, prev, ref, fp=fp)
241 patch.diff(self.repo, prev, ref, fp=fp)
242 difflines = fp.getvalue().splitlines(1)
242 difflines = fp.getvalue().splitlines(1)
243 if self.ui.configbool('notify', 'diffstat', True):
244 s = patch.diffstat(difflines)
245 self.sio.write('\ndiffstat:\n\n' + s)
243 if maxdiff > 0 and len(difflines) > maxdiff:
246 if maxdiff > 0 and len(difflines) > maxdiff:
244 self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
247 self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
245 (len(difflines), maxdiff))
248 (len(difflines), maxdiff))
246 difflines = difflines[:maxdiff]
249 difflines = difflines[:maxdiff]
247 elif difflines:
250 elif difflines:
248 self.sio.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
251 self.sio.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
249 self.sio.write(*difflines)
252 self.sio.write(*difflines)
250
253
251 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
254 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
252 '''send email notifications to interested subscribers.
255 '''send email notifications to interested subscribers.
253
256
254 if used as changegroup hook, send one email for all changesets in
257 if used as changegroup hook, send one email for all changesets in
255 changegroup. else send one email per changeset.'''
258 changegroup. else send one email per changeset.'''
256 n = notifier(ui, repo, hooktype)
259 n = notifier(ui, repo, hooktype)
257 if not n.subs:
260 if not n.subs:
258 ui.debug(_('notify: no subscribers to repo %s\n' % n.root))
261 ui.debug(_('notify: no subscribers to repo %s\n' % n.root))
259 return
262 return
260 if n.skipsource(source):
263 if n.skipsource(source):
261 ui.debug(_('notify: changes have source "%s" - skipping\n') %
264 ui.debug(_('notify: changes have source "%s" - skipping\n') %
262 source)
265 source)
263 return
266 return
264 node = bin(node)
267 node = bin(node)
265 if hooktype == 'changegroup':
268 if hooktype == 'changegroup':
266 start = repo.changelog.rev(node)
269 start = repo.changelog.rev(node)
267 end = repo.changelog.count()
270 end = repo.changelog.count()
268 count = end - start
271 count = end - start
269 for rev in xrange(start, end):
272 for rev in xrange(start, end):
270 n.node(repo.changelog.node(rev))
273 n.node(repo.changelog.node(rev))
271 n.diff(node, repo.changelog.tip())
274 n.diff(node, repo.changelog.tip())
272 else:
275 else:
273 count = 1
276 count = 1
274 n.node(node)
277 n.node(node)
275 n.diff(node, node)
278 n.diff(node, node)
276 n.send(node, count)
279 n.send(node, count)
@@ -1,336 +1,315 b''
1 # Command for sending a collection of Mercurial changesets as a series
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
2 # of patch emails.
3 #
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
5 # which describes the series as a whole.
6 #
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
9 # The message contains two or three body parts:
10 #
10 #
11 # The remainder of the changeset description.
11 # The remainder of the changeset description.
12 #
12 #
13 # [Optional] If the diffstat program is installed, the result of
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
14 # running diffstat on the patch.
15 #
15 #
16 # The patch itself, as generated by "hg export".
16 # The patch itself, as generated by "hg export".
17 #
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
20 # threaded mail and news readers, and in mail archives.
21 #
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
24 # changes.
25 #
25 #
26 # To enable this extension:
26 # To enable this extension:
27 #
27 #
28 # [extensions]
28 # [extensions]
29 # hgext.patchbomb =
29 # hgext.patchbomb =
30 #
30 #
31 # To configure other defaults, add a section like this to your hgrc
31 # To configure other defaults, add a section like this to your hgrc
32 # file:
32 # file:
33 #
33 #
34 # [email]
34 # [email]
35 # from = My Name <my@email>
35 # from = My Name <my@email>
36 # to = recipient1, recipient2, ...
36 # to = recipient1, recipient2, ...
37 # cc = cc1, cc2, ...
37 # cc = cc1, cc2, ...
38 # bcc = bcc1, bcc2, ...
38 # bcc = bcc1, bcc2, ...
39 #
39 #
40 # Then you can use the "hg email" command to mail a series of changesets
40 # Then you can use the "hg email" command to mail a series of changesets
41 # as a patchbomb.
41 # as a patchbomb.
42 #
42 #
43 # To avoid sending patches prematurely, it is a good idea to first run
43 # To avoid sending patches prematurely, it is a good idea to first run
44 # the "email" command with the "-n" option (test only). You will be
44 # the "email" command with the "-n" option (test only). You will be
45 # prompted for an email recipient address, a subject an an introductory
45 # prompted for an email recipient address, a subject an an introductory
46 # message describing the patches of your patchbomb. Then when all is
46 # message describing the patches of your patchbomb. Then when all is
47 # done, your pager will be fired up once for each patchbomb message, so
47 # done, your pager will be fired up once for each patchbomb message, so
48 # you can verify everything is alright.
48 # you can verify everything is alright.
49 #
49 #
50 # The "-m" (mbox) option is also very useful. Instead of previewing
50 # The "-m" (mbox) option is also very useful. Instead of previewing
51 # each patchbomb message in a pager or sending the messages directly,
51 # each patchbomb message in a pager or sending the messages directly,
52 # it will create a UNIX mailbox file with the patch emails. This
52 # it will create a UNIX mailbox file with the patch emails. This
53 # mailbox file can be previewed with any mail user agent which supports
53 # mailbox file can be previewed with any mail user agent which supports
54 # UNIX mbox files, i.e. with mutt:
54 # UNIX mbox files, i.e. with mutt:
55 #
55 #
56 # % mutt -R -f mbox
56 # % mutt -R -f mbox
57 #
57 #
58 # When you are previewing the patchbomb messages, you can use `formail'
58 # When you are previewing the patchbomb messages, you can use `formail'
59 # (a utility that is commonly installed as part of the procmail package),
59 # (a utility that is commonly installed as part of the procmail package),
60 # to send each message out:
60 # to send each message out:
61 #
61 #
62 # % formail -s sendmail -bm -t < mbox
62 # % formail -s sendmail -bm -t < mbox
63 #
63 #
64 # That should be all. Now your patchbomb is on its way out.
64 # That should be all. Now your patchbomb is on its way out.
65
65
66 from mercurial.demandload import *
66 from mercurial.demandload import *
67 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
67 demandload(globals(), '''email.MIMEMultipart email.MIMEText email.Utils
68 mercurial:commands,hg,mail,ui
68 mercurial:commands,hg,mail,ui,patch
69 os errno popen2 socket sys tempfile time''')
69 os errno popen2 socket sys tempfile time''')
70 from mercurial.i18n import gettext as _
70 from mercurial.i18n import gettext as _
71 from mercurial.node import *
71 from mercurial.node import *
72
72
73 try:
73 try:
74 # readline gives raw_input editing capabilities, but is not
74 # readline gives raw_input editing capabilities, but is not
75 # present on windows
75 # present on windows
76 import readline
76 import readline
77 except ImportError: pass
77 except ImportError: pass
78
78
79 def diffstat(patch):
80 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
81 try:
82 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
83 try:
84 for line in patch: print >> p.tochild, line
85 p.tochild.close()
86 if p.wait(): return
87 fp = os.fdopen(fd, 'r')
88 stat = []
89 for line in fp: stat.append(line.lstrip())
90 last = stat.pop()
91 stat.insert(0, last)
92 stat = ''.join(stat)
93 if stat.startswith('0 files'): raise ValueError
94 return stat
95 except: raise
96 finally:
97 try: os.unlink(name)
98 except: pass
99
100 def patchbomb(ui, repo, *revs, **opts):
79 def patchbomb(ui, repo, *revs, **opts):
101 '''send changesets as a series of patch emails
80 '''send changesets as a series of patch emails
102
81
103 The series starts with a "[PATCH 0 of N]" introduction, which
82 The series starts with a "[PATCH 0 of N]" introduction, which
104 describes the series as a whole.
83 describes the series as a whole.
105
84
106 Each patch email has a Subject line of "[PATCH M of N] ...", using
85 Each patch email has a Subject line of "[PATCH M of N] ...", using
107 the first line of the changeset description as the subject text.
86 the first line of the changeset description as the subject text.
108 The message contains two or three body parts. First, the rest of
87 The message contains two or three body parts. First, the rest of
109 the changeset description. Next, (optionally) if the diffstat
88 the changeset description. Next, (optionally) if the diffstat
110 program is installed, the result of running diffstat on the patch.
89 program is installed, the result of running diffstat on the patch.
111 Finally, the patch itself, as generated by "hg export".'''
90 Finally, the patch itself, as generated by "hg export".'''
112 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
91 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
113 if default: prompt += ' [%s]' % default
92 if default: prompt += ' [%s]' % default
114 prompt += rest
93 prompt += rest
115 while True:
94 while True:
116 r = raw_input(prompt)
95 r = raw_input(prompt)
117 if r: return r
96 if r: return r
118 if default is not None: return default
97 if default is not None: return default
119 if empty_ok: return r
98 if empty_ok: return r
120 ui.warn(_('Please enter a valid value.\n'))
99 ui.warn(_('Please enter a valid value.\n'))
121
100
122 def confirm(s):
101 def confirm(s):
123 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
102 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
124 raise ValueError
103 raise ValueError
125
104
126 def cdiffstat(summary, patch):
105 def cdiffstat(summary, patchlines):
127 s = diffstat(patch)
106 s = patch.diffstat(patchlines)
128 if s:
107 if s:
129 if summary:
108 if summary:
130 ui.write(summary, '\n')
109 ui.write(summary, '\n')
131 ui.write(s, '\n')
110 ui.write(s, '\n')
132 confirm(_('Does the diffstat above look okay'))
111 confirm(_('Does the diffstat above look okay'))
133 return s
112 return s
134
113
135 def makepatch(patch, idx, total):
114 def makepatch(patch, idx, total):
136 desc = []
115 desc = []
137 node = None
116 node = None
138 body = ''
117 body = ''
139 for line in patch:
118 for line in patch:
140 if line.startswith('#'):
119 if line.startswith('#'):
141 if line.startswith('# Node ID'): node = line.split()[-1]
120 if line.startswith('# Node ID'): node = line.split()[-1]
142 continue
121 continue
143 if (line.startswith('diff -r')
122 if (line.startswith('diff -r')
144 or line.startswith('diff --git')):
123 or line.startswith('diff --git')):
145 break
124 break
146 desc.append(line)
125 desc.append(line)
147 if not node: raise ValueError
126 if not node: raise ValueError
148
127
149 #body = ('\n'.join(desc[1:]).strip() or
128 #body = ('\n'.join(desc[1:]).strip() or
150 # 'Patch subject is complete summary.')
129 # 'Patch subject is complete summary.')
151 #body += '\n\n\n'
130 #body += '\n\n\n'
152
131
153 if opts['plain']:
132 if opts['plain']:
154 while patch and patch[0].startswith('# '): patch.pop(0)
133 while patch and patch[0].startswith('# '): patch.pop(0)
155 if patch: patch.pop(0)
134 if patch: patch.pop(0)
156 while patch and not patch[0].strip(): patch.pop(0)
135 while patch and not patch[0].strip(): patch.pop(0)
157 if opts['diffstat']:
136 if opts['diffstat']:
158 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
137 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
159 if opts['attach']:
138 if opts['attach']:
160 msg = email.MIMEMultipart.MIMEMultipart()
139 msg = email.MIMEMultipart.MIMEMultipart()
161 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
140 if body: msg.attach(email.MIMEText.MIMEText(body, 'plain'))
162 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
141 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
163 binnode = bin(node)
142 binnode = bin(node)
164 # if node is mq patch, it will have patch file name as tag
143 # if node is mq patch, it will have patch file name as tag
165 patchname = [t for t in repo.nodetags(binnode)
144 patchname = [t for t in repo.nodetags(binnode)
166 if t.endswith('.patch') or t.endswith('.diff')]
145 if t.endswith('.patch') or t.endswith('.diff')]
167 if patchname:
146 if patchname:
168 patchname = patchname[0]
147 patchname = patchname[0]
169 elif total > 1:
148 elif total > 1:
170 patchname = commands.make_filename(repo, '%b-%n.patch',
149 patchname = commands.make_filename(repo, '%b-%n.patch',
171 binnode, idx, total)
150 binnode, idx, total)
172 else:
151 else:
173 patchname = commands.make_filename(repo, '%b.patch', binnode)
152 patchname = commands.make_filename(repo, '%b.patch', binnode)
174 p['Content-Disposition'] = 'inline; filename=' + patchname
153 p['Content-Disposition'] = 'inline; filename=' + patchname
175 msg.attach(p)
154 msg.attach(p)
176 else:
155 else:
177 body += '\n'.join(patch)
156 body += '\n'.join(patch)
178 msg = email.MIMEText.MIMEText(body)
157 msg = email.MIMEText.MIMEText(body)
179 if total == 1:
158 if total == 1:
180 subj = '[PATCH] ' + desc[0].strip()
159 subj = '[PATCH] ' + desc[0].strip()
181 else:
160 else:
182 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
161 subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
183 if subj.endswith('.'): subj = subj[:-1]
162 if subj.endswith('.'): subj = subj[:-1]
184 msg['Subject'] = subj
163 msg['Subject'] = subj
185 msg['X-Mercurial-Node'] = node
164 msg['X-Mercurial-Node'] = node
186 return msg
165 return msg
187
166
188 start_time = int(time.time())
167 start_time = int(time.time())
189
168
190 def genmsgid(id):
169 def genmsgid(id):
191 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
170 return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
192
171
193 patches = []
172 patches = []
194
173
195 class exportee:
174 class exportee:
196 def __init__(self, container):
175 def __init__(self, container):
197 self.lines = []
176 self.lines = []
198 self.container = container
177 self.container = container
199 self.name = 'email'
178 self.name = 'email'
200
179
201 def write(self, data):
180 def write(self, data):
202 self.lines.append(data)
181 self.lines.append(data)
203
182
204 def close(self):
183 def close(self):
205 self.container.append(''.join(self.lines).split('\n'))
184 self.container.append(''.join(self.lines).split('\n'))
206 self.lines = []
185 self.lines = []
207
186
208 commands.export(ui, repo, *revs, **{'output': exportee(patches),
187 commands.export(ui, repo, *revs, **{'output': exportee(patches),
209 'switch_parent': False,
188 'switch_parent': False,
210 'text': None,
189 'text': None,
211 'git': opts.get('git')})
190 'git': opts.get('git')})
212
191
213 jumbo = []
192 jumbo = []
214 msgs = []
193 msgs = []
215
194
216 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
195 ui.write(_('This patch series consists of %d patches.\n\n') % len(patches))
217
196
218 for p, i in zip(patches, range(len(patches))):
197 for p, i in zip(patches, range(len(patches))):
219 jumbo.extend(p)
198 jumbo.extend(p)
220 msgs.append(makepatch(p, i + 1, len(patches)))
199 msgs.append(makepatch(p, i + 1, len(patches)))
221
200
222 sender = (opts['from'] or ui.config('email', 'from') or
201 sender = (opts['from'] or ui.config('email', 'from') or
223 ui.config('patchbomb', 'from') or
202 ui.config('patchbomb', 'from') or
224 prompt('From', ui.username()))
203 prompt('From', ui.username()))
225
204
226 def getaddrs(opt, prpt, default = None):
205 def getaddrs(opt, prpt, default = None):
227 addrs = opts[opt] or (ui.config('email', opt) or
206 addrs = opts[opt] or (ui.config('email', opt) or
228 ui.config('patchbomb', opt) or
207 ui.config('patchbomb', opt) or
229 prompt(prpt, default = default)).split(',')
208 prompt(prpt, default = default)).split(',')
230 return [a.strip() for a in addrs if a.strip()]
209 return [a.strip() for a in addrs if a.strip()]
231 to = getaddrs('to', 'To')
210 to = getaddrs('to', 'To')
232 cc = getaddrs('cc', 'Cc', '')
211 cc = getaddrs('cc', 'Cc', '')
233
212
234 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
213 bcc = opts['bcc'] or (ui.config('email', 'bcc') or
235 ui.config('patchbomb', 'bcc') or '').split(',')
214 ui.config('patchbomb', 'bcc') or '').split(',')
236 bcc = [a.strip() for a in bcc if a.strip()]
215 bcc = [a.strip() for a in bcc if a.strip()]
237
216
238 if len(patches) > 1:
217 if len(patches) > 1:
239 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
218 ui.write(_('\nWrite the introductory message for the patch series.\n\n'))
240
219
241 subj = '[PATCH 0 of %d] %s' % (
220 subj = '[PATCH 0 of %d] %s' % (
242 len(patches),
221 len(patches),
243 opts['subject'] or
222 opts['subject'] or
244 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
223 prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
245
224
246 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
225 ui.write(_('Finish with ^D or a dot on a line by itself.\n\n'))
247
226
248 body = []
227 body = []
249
228
250 while True:
229 while True:
251 try: l = raw_input()
230 try: l = raw_input()
252 except EOFError: break
231 except EOFError: break
253 if l == '.': break
232 if l == '.': break
254 body.append(l)
233 body.append(l)
255
234
256 if opts['diffstat']:
235 if opts['diffstat']:
257 d = cdiffstat(_('Final summary:\n'), jumbo)
236 d = cdiffstat(_('Final summary:\n'), jumbo)
258 if d: body.append('\n' + d)
237 if d: body.append('\n' + d)
259
238
260 body = '\n'.join(body) + '\n'
239 body = '\n'.join(body) + '\n'
261
240
262 msg = email.MIMEText.MIMEText(body)
241 msg = email.MIMEText.MIMEText(body)
263 msg['Subject'] = subj
242 msg['Subject'] = subj
264
243
265 msgs.insert(0, msg)
244 msgs.insert(0, msg)
266
245
267 ui.write('\n')
246 ui.write('\n')
268
247
269 if not opts['test'] and not opts['mbox']:
248 if not opts['test'] and not opts['mbox']:
270 mailer = mail.connect(ui)
249 mailer = mail.connect(ui)
271 parent = None
250 parent = None
272
251
273 # Calculate UTC offset
252 # Calculate UTC offset
274 if time.daylight: offset = time.altzone
253 if time.daylight: offset = time.altzone
275 else: offset = time.timezone
254 else: offset = time.timezone
276 if offset <= 0: sign, offset = '+', -offset
255 if offset <= 0: sign, offset = '+', -offset
277 else: sign = '-'
256 else: sign = '-'
278 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
257 offset = '%s%02d%02d' % (sign, offset / 3600, (offset % 3600) / 60)
279
258
280 sender_addr = email.Utils.parseaddr(sender)[1]
259 sender_addr = email.Utils.parseaddr(sender)[1]
281 for m in msgs:
260 for m in msgs:
282 try:
261 try:
283 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
262 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
284 except TypeError:
263 except TypeError:
285 m['Message-Id'] = genmsgid('patchbomb')
264 m['Message-Id'] = genmsgid('patchbomb')
286 if parent:
265 if parent:
287 m['In-Reply-To'] = parent
266 m['In-Reply-To'] = parent
288 else:
267 else:
289 parent = m['Message-Id']
268 parent = m['Message-Id']
290 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
269 m['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(start_time)) + ' ' + offset
291
270
292 start_time += 1
271 start_time += 1
293 m['From'] = sender
272 m['From'] = sender
294 m['To'] = ', '.join(to)
273 m['To'] = ', '.join(to)
295 if cc: m['Cc'] = ', '.join(cc)
274 if cc: m['Cc'] = ', '.join(cc)
296 if bcc: m['Bcc'] = ', '.join(bcc)
275 if bcc: m['Bcc'] = ', '.join(bcc)
297 if opts['test']:
276 if opts['test']:
298 ui.status('Displaying ', m['Subject'], ' ...\n')
277 ui.status('Displaying ', m['Subject'], ' ...\n')
299 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
278 fp = os.popen(os.getenv('PAGER', 'more'), 'w')
300 try:
279 try:
301 fp.write(m.as_string(0))
280 fp.write(m.as_string(0))
302 fp.write('\n')
281 fp.write('\n')
303 except IOError, inst:
282 except IOError, inst:
304 if inst.errno != errno.EPIPE:
283 if inst.errno != errno.EPIPE:
305 raise
284 raise
306 fp.close()
285 fp.close()
307 elif opts['mbox']:
286 elif opts['mbox']:
308 ui.status('Writing ', m['Subject'], ' ...\n')
287 ui.status('Writing ', m['Subject'], ' ...\n')
309 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
288 fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+')
310 date = time.asctime(time.localtime(start_time))
289 date = time.asctime(time.localtime(start_time))
311 fp.write('From %s %s\n' % (sender_addr, date))
290 fp.write('From %s %s\n' % (sender_addr, date))
312 fp.write(m.as_string(0))
291 fp.write(m.as_string(0))
313 fp.write('\n\n')
292 fp.write('\n\n')
314 fp.close()
293 fp.close()
315 else:
294 else:
316 ui.status('Sending ', m['Subject'], ' ...\n')
295 ui.status('Sending ', m['Subject'], ' ...\n')
317 # Exim does not remove the Bcc field
296 # Exim does not remove the Bcc field
318 del m['Bcc']
297 del m['Bcc']
319 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
298 mailer.sendmail(sender, to + bcc + cc, m.as_string(0))
320
299
321 cmdtable = {
300 cmdtable = {
322 'email':
301 'email':
323 (patchbomb,
302 (patchbomb,
324 [('a', 'attach', None, 'send patches as inline attachments'),
303 [('a', 'attach', None, 'send patches as inline attachments'),
325 ('', 'bcc', [], 'email addresses of blind copy recipients'),
304 ('', 'bcc', [], 'email addresses of blind copy recipients'),
326 ('c', 'cc', [], 'email addresses of copy recipients'),
305 ('c', 'cc', [], 'email addresses of copy recipients'),
327 ('d', 'diffstat', None, 'add diffstat output to messages'),
306 ('d', 'diffstat', None, 'add diffstat output to messages'),
328 ('g', 'git', None, _('use git extended diff format')),
307 ('g', 'git', None, _('use git extended diff format')),
329 ('f', 'from', '', 'email address of sender'),
308 ('f', 'from', '', 'email address of sender'),
330 ('', 'plain', None, 'omit hg patch header'),
309 ('', 'plain', None, 'omit hg patch header'),
331 ('n', 'test', None, 'print messages that would be sent'),
310 ('n', 'test', None, 'print messages that would be sent'),
332 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
311 ('m', 'mbox', '', 'write messages to mbox file instead of sending them'),
333 ('s', 'subject', '', 'subject of introductory message'),
312 ('s', 'subject', '', 'subject of introductory message'),
334 ('t', 'to', [], 'email addresses of recipients')],
313 ('t', 'to', [], 'email addresses of recipients')],
335 "hg email [OPTION]... [REV]...")
314 "hg email [OPTION]... [REV]...")
336 }
315 }
@@ -1,552 +1,574 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from demandload import demandload
8 from demandload import demandload
9 from i18n import gettext as _
9 from i18n import gettext as _
10 from node import *
10 from node import *
11 demandload(globals(), "cmdutil mdiff util")
11 demandload(globals(), "cmdutil mdiff util")
12 demandload(globals(), "cStringIO email.Parser errno os re shutil sys tempfile")
12 demandload(globals(), '''cStringIO email.Parser errno os re shutil sys tempfile
13 popen2''')
13
14
14 # helper functions
15 # helper functions
15
16
16 def copyfile(src, dst, basedir=None):
17 def copyfile(src, dst, basedir=None):
17 if not basedir:
18 if not basedir:
18 basedir = os.getcwd()
19 basedir = os.getcwd()
19
20
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 if os.path.exists(absdst):
22 if os.path.exists(absdst):
22 raise util.Abort(_("cannot create %s: destination already exists") %
23 raise util.Abort(_("cannot create %s: destination already exists") %
23 dst)
24 dst)
24
25
25 targetdir = os.path.dirname(absdst)
26 targetdir = os.path.dirname(absdst)
26 if not os.path.isdir(targetdir):
27 if not os.path.isdir(targetdir):
27 os.makedirs(targetdir)
28 os.makedirs(targetdir)
28 try:
29 try:
29 shutil.copyfile(abssrc, absdst)
30 shutil.copyfile(abssrc, absdst)
30 shutil.copymode(abssrc, absdst)
31 shutil.copymode(abssrc, absdst)
31 except shutil.Error, inst:
32 except shutil.Error, inst:
32 raise util.Abort(str(inst))
33 raise util.Abort(str(inst))
33
34
34 # public functions
35 # public functions
35
36
36 def extract(ui, fileobj):
37 def extract(ui, fileobj):
37 '''extract patch from data read from fileobj.
38 '''extract patch from data read from fileobj.
38
39
39 patch can be normal patch or contained in email message.
40 patch can be normal patch or contained in email message.
40
41
41 return tuple (filename, message, user, date). any item in returned
42 return tuple (filename, message, user, date). any item in returned
42 tuple can be None. if filename is None, fileobj did not contain
43 tuple can be None. if filename is None, fileobj did not contain
43 patch. caller must unlink filename when done.'''
44 patch. caller must unlink filename when done.'''
44
45
45 # attempt to detect the start of a patch
46 # attempt to detect the start of a patch
46 # (this heuristic is borrowed from quilt)
47 # (this heuristic is borrowed from quilt)
47 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
48 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
48 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
49 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
49 '(---|\*\*\*)[ \t])', re.MULTILINE)
50 '(---|\*\*\*)[ \t])', re.MULTILINE)
50
51
51 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
52 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
52 tmpfp = os.fdopen(fd, 'w')
53 tmpfp = os.fdopen(fd, 'w')
53 try:
54 try:
54 hgpatch = False
55 hgpatch = False
55
56
56 msg = email.Parser.Parser().parse(fileobj)
57 msg = email.Parser.Parser().parse(fileobj)
57
58
58 message = msg['Subject']
59 message = msg['Subject']
59 user = msg['From']
60 user = msg['From']
60 # should try to parse msg['Date']
61 # should try to parse msg['Date']
61 date = None
62 date = None
62
63
63 if message:
64 if message:
64 message = message.replace('\n\t', ' ')
65 message = message.replace('\n\t', ' ')
65 ui.debug('Subject: %s\n' % message)
66 ui.debug('Subject: %s\n' % message)
66 if user:
67 if user:
67 ui.debug('From: %s\n' % user)
68 ui.debug('From: %s\n' % user)
68 diffs_seen = 0
69 diffs_seen = 0
69 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
70 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
70
71
71 for part in msg.walk():
72 for part in msg.walk():
72 content_type = part.get_content_type()
73 content_type = part.get_content_type()
73 ui.debug('Content-Type: %s\n' % content_type)
74 ui.debug('Content-Type: %s\n' % content_type)
74 if content_type not in ok_types:
75 if content_type not in ok_types:
75 continue
76 continue
76 payload = part.get_payload(decode=True)
77 payload = part.get_payload(decode=True)
77 m = diffre.search(payload)
78 m = diffre.search(payload)
78 if m:
79 if m:
79 ui.debug(_('found patch at byte %d\n') % m.start(0))
80 ui.debug(_('found patch at byte %d\n') % m.start(0))
80 diffs_seen += 1
81 diffs_seen += 1
81 cfp = cStringIO.StringIO()
82 cfp = cStringIO.StringIO()
82 if message:
83 if message:
83 cfp.write(message)
84 cfp.write(message)
84 cfp.write('\n')
85 cfp.write('\n')
85 for line in payload[:m.start(0)].splitlines():
86 for line in payload[:m.start(0)].splitlines():
86 if line.startswith('# HG changeset patch'):
87 if line.startswith('# HG changeset patch'):
87 ui.debug(_('patch generated by hg export\n'))
88 ui.debug(_('patch generated by hg export\n'))
88 hgpatch = True
89 hgpatch = True
89 # drop earlier commit message content
90 # drop earlier commit message content
90 cfp.seek(0)
91 cfp.seek(0)
91 cfp.truncate()
92 cfp.truncate()
92 elif hgpatch:
93 elif hgpatch:
93 if line.startswith('# User '):
94 if line.startswith('# User '):
94 user = line[7:]
95 user = line[7:]
95 ui.debug('From: %s\n' % user)
96 ui.debug('From: %s\n' % user)
96 elif line.startswith("# Date "):
97 elif line.startswith("# Date "):
97 date = line[7:]
98 date = line[7:]
98 if not line.startswith('# '):
99 if not line.startswith('# '):
99 cfp.write(line)
100 cfp.write(line)
100 cfp.write('\n')
101 cfp.write('\n')
101 message = cfp.getvalue()
102 message = cfp.getvalue()
102 if tmpfp:
103 if tmpfp:
103 tmpfp.write(payload)
104 tmpfp.write(payload)
104 if not payload.endswith('\n'):
105 if not payload.endswith('\n'):
105 tmpfp.write('\n')
106 tmpfp.write('\n')
106 elif not diffs_seen and message and content_type == 'text/plain':
107 elif not diffs_seen and message and content_type == 'text/plain':
107 message += '\n' + payload
108 message += '\n' + payload
108 except:
109 except:
109 tmpfp.close()
110 tmpfp.close()
110 os.unlink(tmpname)
111 os.unlink(tmpname)
111 raise
112 raise
112
113
113 tmpfp.close()
114 tmpfp.close()
114 if not diffs_seen:
115 if not diffs_seen:
115 os.unlink(tmpname)
116 os.unlink(tmpname)
116 return None, message, user, date
117 return None, message, user, date
117 return tmpname, message, user, date
118 return tmpname, message, user, date
118
119
119 def readgitpatch(patchname):
120 def readgitpatch(patchname):
120 """extract git-style metadata about patches from <patchname>"""
121 """extract git-style metadata about patches from <patchname>"""
121 class gitpatch:
122 class gitpatch:
122 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
123 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
123 def __init__(self, path):
124 def __init__(self, path):
124 self.path = path
125 self.path = path
125 self.oldpath = None
126 self.oldpath = None
126 self.mode = None
127 self.mode = None
127 self.op = 'MODIFY'
128 self.op = 'MODIFY'
128 self.copymod = False
129 self.copymod = False
129 self.lineno = 0
130 self.lineno = 0
130
131
131 # Filter patch for git information
132 # Filter patch for git information
132 gitre = re.compile('diff --git a/(.*) b/(.*)')
133 gitre = re.compile('diff --git a/(.*) b/(.*)')
133 pf = file(patchname)
134 pf = file(patchname)
134 gp = None
135 gp = None
135 gitpatches = []
136 gitpatches = []
136 # Can have a git patch with only metadata, causing patch to complain
137 # Can have a git patch with only metadata, causing patch to complain
137 dopatch = False
138 dopatch = False
138
139
139 lineno = 0
140 lineno = 0
140 for line in pf:
141 for line in pf:
141 lineno += 1
142 lineno += 1
142 if line.startswith('diff --git'):
143 if line.startswith('diff --git'):
143 m = gitre.match(line)
144 m = gitre.match(line)
144 if m:
145 if m:
145 if gp:
146 if gp:
146 gitpatches.append(gp)
147 gitpatches.append(gp)
147 src, dst = m.group(1,2)
148 src, dst = m.group(1,2)
148 gp = gitpatch(dst)
149 gp = gitpatch(dst)
149 gp.lineno = lineno
150 gp.lineno = lineno
150 elif gp:
151 elif gp:
151 if line.startswith('--- '):
152 if line.startswith('--- '):
152 if gp.op in ('COPY', 'RENAME'):
153 if gp.op in ('COPY', 'RENAME'):
153 gp.copymod = True
154 gp.copymod = True
154 dopatch = 'filter'
155 dopatch = 'filter'
155 gitpatches.append(gp)
156 gitpatches.append(gp)
156 gp = None
157 gp = None
157 if not dopatch:
158 if not dopatch:
158 dopatch = True
159 dopatch = True
159 continue
160 continue
160 if line.startswith('rename from '):
161 if line.startswith('rename from '):
161 gp.op = 'RENAME'
162 gp.op = 'RENAME'
162 gp.oldpath = line[12:].rstrip()
163 gp.oldpath = line[12:].rstrip()
163 elif line.startswith('rename to '):
164 elif line.startswith('rename to '):
164 gp.path = line[10:].rstrip()
165 gp.path = line[10:].rstrip()
165 elif line.startswith('copy from '):
166 elif line.startswith('copy from '):
166 gp.op = 'COPY'
167 gp.op = 'COPY'
167 gp.oldpath = line[10:].rstrip()
168 gp.oldpath = line[10:].rstrip()
168 elif line.startswith('copy to '):
169 elif line.startswith('copy to '):
169 gp.path = line[8:].rstrip()
170 gp.path = line[8:].rstrip()
170 elif line.startswith('deleted file'):
171 elif line.startswith('deleted file'):
171 gp.op = 'DELETE'
172 gp.op = 'DELETE'
172 elif line.startswith('new file mode '):
173 elif line.startswith('new file mode '):
173 gp.op = 'ADD'
174 gp.op = 'ADD'
174 gp.mode = int(line.rstrip()[-3:], 8)
175 gp.mode = int(line.rstrip()[-3:], 8)
175 elif line.startswith('new mode '):
176 elif line.startswith('new mode '):
176 gp.mode = int(line.rstrip()[-3:], 8)
177 gp.mode = int(line.rstrip()[-3:], 8)
177 if gp:
178 if gp:
178 gitpatches.append(gp)
179 gitpatches.append(gp)
179
180
180 if not gitpatches:
181 if not gitpatches:
181 dopatch = True
182 dopatch = True
182
183
183 return (dopatch, gitpatches)
184 return (dopatch, gitpatches)
184
185
185 def dogitpatch(patchname, gitpatches, cwd=None):
186 def dogitpatch(patchname, gitpatches, cwd=None):
186 """Preprocess git patch so that vanilla patch can handle it"""
187 """Preprocess git patch so that vanilla patch can handle it"""
187 pf = file(patchname)
188 pf = file(patchname)
188 pfline = 1
189 pfline = 1
189
190
190 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
191 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
191 tmpfp = os.fdopen(fd, 'w')
192 tmpfp = os.fdopen(fd, 'w')
192
193
193 try:
194 try:
194 for i in range(len(gitpatches)):
195 for i in range(len(gitpatches)):
195 p = gitpatches[i]
196 p = gitpatches[i]
196 if not p.copymod:
197 if not p.copymod:
197 continue
198 continue
198
199
199 copyfile(p.oldpath, p.path, basedir=cwd)
200 copyfile(p.oldpath, p.path, basedir=cwd)
200
201
201 # rewrite patch hunk
202 # rewrite patch hunk
202 while pfline < p.lineno:
203 while pfline < p.lineno:
203 tmpfp.write(pf.readline())
204 tmpfp.write(pf.readline())
204 pfline += 1
205 pfline += 1
205 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
206 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
206 line = pf.readline()
207 line = pf.readline()
207 pfline += 1
208 pfline += 1
208 while not line.startswith('--- a/'):
209 while not line.startswith('--- a/'):
209 tmpfp.write(line)
210 tmpfp.write(line)
210 line = pf.readline()
211 line = pf.readline()
211 pfline += 1
212 pfline += 1
212 tmpfp.write('--- a/%s\n' % p.path)
213 tmpfp.write('--- a/%s\n' % p.path)
213
214
214 line = pf.readline()
215 line = pf.readline()
215 while line:
216 while line:
216 tmpfp.write(line)
217 tmpfp.write(line)
217 line = pf.readline()
218 line = pf.readline()
218 except:
219 except:
219 tmpfp.close()
220 tmpfp.close()
220 os.unlink(patchname)
221 os.unlink(patchname)
221 raise
222 raise
222
223
223 tmpfp.close()
224 tmpfp.close()
224 return patchname
225 return patchname
225
226
226 def patch(patchname, ui, strip=1, cwd=None):
227 def patch(patchname, ui, strip=1, cwd=None):
227 """apply the patch <patchname> to the working directory.
228 """apply the patch <patchname> to the working directory.
228 a list of patched files is returned"""
229 a list of patched files is returned"""
229
230
230 # helper function
231 # helper function
231 def __patch(patchname):
232 def __patch(patchname):
232 """patch and updates the files and fuzz variables"""
233 """patch and updates the files and fuzz variables"""
233 files = {}
234 files = {}
234 fuzz = False
235 fuzz = False
235
236
236 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
237 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
237 'patch')
238 'patch')
238 args = []
239 args = []
239 if cwd:
240 if cwd:
240 args.append('-d %s' % util.shellquote(cwd))
241 args.append('-d %s' % util.shellquote(cwd))
241 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
242 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
242 util.shellquote(patchname)))
243 util.shellquote(patchname)))
243
244
244 for line in fp:
245 for line in fp:
245 line = line.rstrip()
246 line = line.rstrip()
246 ui.note(line + '\n')
247 ui.note(line + '\n')
247 if line.startswith('patching file '):
248 if line.startswith('patching file '):
248 pf = util.parse_patch_output(line)
249 pf = util.parse_patch_output(line)
249 printed_file = False
250 printed_file = False
250 files.setdefault(pf, (None, None))
251 files.setdefault(pf, (None, None))
251 elif line.find('with fuzz') >= 0:
252 elif line.find('with fuzz') >= 0:
252 fuzz = True
253 fuzz = True
253 if not printed_file:
254 if not printed_file:
254 ui.warn(pf + '\n')
255 ui.warn(pf + '\n')
255 printed_file = True
256 printed_file = True
256 ui.warn(line + '\n')
257 ui.warn(line + '\n')
257 elif line.find('saving rejects to file') >= 0:
258 elif line.find('saving rejects to file') >= 0:
258 ui.warn(line + '\n')
259 ui.warn(line + '\n')
259 elif line.find('FAILED') >= 0:
260 elif line.find('FAILED') >= 0:
260 if not printed_file:
261 if not printed_file:
261 ui.warn(pf + '\n')
262 ui.warn(pf + '\n')
262 printed_file = True
263 printed_file = True
263 ui.warn(line + '\n')
264 ui.warn(line + '\n')
264 code = fp.close()
265 code = fp.close()
265 if code:
266 if code:
266 raise util.Abort(_("patch command failed: %s") %
267 raise util.Abort(_("patch command failed: %s") %
267 util.explain_exit(code)[0])
268 util.explain_exit(code)[0])
268 return files, fuzz
269 return files, fuzz
269
270
270 (dopatch, gitpatches) = readgitpatch(patchname)
271 (dopatch, gitpatches) = readgitpatch(patchname)
271
272
272 if dopatch:
273 if dopatch:
273 if dopatch == 'filter':
274 if dopatch == 'filter':
274 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
275 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
275 try:
276 try:
276 files, fuzz = __patch(patchname)
277 files, fuzz = __patch(patchname)
277 finally:
278 finally:
278 if dopatch == 'filter':
279 if dopatch == 'filter':
279 os.unlink(patchname)
280 os.unlink(patchname)
280 else:
281 else:
281 files, fuzz = {}, False
282 files, fuzz = {}, False
282
283
283 for gp in gitpatches:
284 for gp in gitpatches:
284 files[gp.path] = (gp.op, gp)
285 files[gp.path] = (gp.op, gp)
285
286
286 return (files, fuzz)
287 return (files, fuzz)
287
288
288 def diffopts(ui, opts={}):
289 def diffopts(ui, opts={}):
289 return mdiff.diffopts(
290 return mdiff.diffopts(
290 text=opts.get('text'),
291 text=opts.get('text'),
291 git=(opts.get('git') or
292 git=(opts.get('git') or
292 ui.configbool('diff', 'git', None)),
293 ui.configbool('diff', 'git', None)),
293 showfunc=(opts.get('show_function') or
294 showfunc=(opts.get('show_function') or
294 ui.configbool('diff', 'showfunc', None)),
295 ui.configbool('diff', 'showfunc', None)),
295 ignorews=(opts.get('ignore_all_space') or
296 ignorews=(opts.get('ignore_all_space') or
296 ui.configbool('diff', 'ignorews', None)),
297 ui.configbool('diff', 'ignorews', None)),
297 ignorewsamount=(opts.get('ignore_space_change') or
298 ignorewsamount=(opts.get('ignore_space_change') or
298 ui.configbool('diff', 'ignorewsamount', None)),
299 ui.configbool('diff', 'ignorewsamount', None)),
299 ignoreblanklines=(opts.get('ignore_blank_lines') or
300 ignoreblanklines=(opts.get('ignore_blank_lines') or
300 ui.configbool('diff', 'ignoreblanklines', None)))
301 ui.configbool('diff', 'ignoreblanklines', None)))
301
302
302 def updatedir(ui, repo, patches, wlock=None):
303 def updatedir(ui, repo, patches, wlock=None):
303 '''Update dirstate after patch application according to metadata'''
304 '''Update dirstate after patch application according to metadata'''
304 if not patches:
305 if not patches:
305 return
306 return
306 copies = []
307 copies = []
307 removes = []
308 removes = []
308 cfiles = patches.keys()
309 cfiles = patches.keys()
309 copts = {'after': False, 'force': False}
310 copts = {'after': False, 'force': False}
310 cwd = repo.getcwd()
311 cwd = repo.getcwd()
311 if cwd:
312 if cwd:
312 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
313 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
313 for f in patches:
314 for f in patches:
314 ctype, gp = patches[f]
315 ctype, gp = patches[f]
315 if ctype == 'RENAME':
316 if ctype == 'RENAME':
316 copies.append((gp.oldpath, gp.path, gp.copymod))
317 copies.append((gp.oldpath, gp.path, gp.copymod))
317 removes.append(gp.oldpath)
318 removes.append(gp.oldpath)
318 elif ctype == 'COPY':
319 elif ctype == 'COPY':
319 copies.append((gp.oldpath, gp.path, gp.copymod))
320 copies.append((gp.oldpath, gp.path, gp.copymod))
320 elif ctype == 'DELETE':
321 elif ctype == 'DELETE':
321 removes.append(gp.path)
322 removes.append(gp.path)
322 for src, dst, after in copies:
323 for src, dst, after in copies:
323 if not after:
324 if not after:
324 copyfile(src, dst, repo.root)
325 copyfile(src, dst, repo.root)
325 repo.copy(src, dst, wlock=wlock)
326 repo.copy(src, dst, wlock=wlock)
326 if removes:
327 if removes:
327 repo.remove(removes, True, wlock=wlock)
328 repo.remove(removes, True, wlock=wlock)
328 for f in patches:
329 for f in patches:
329 ctype, gp = patches[f]
330 ctype, gp = patches[f]
330 if gp and gp.mode:
331 if gp and gp.mode:
331 x = gp.mode & 0100 != 0
332 x = gp.mode & 0100 != 0
332 dst = os.path.join(repo.root, gp.path)
333 dst = os.path.join(repo.root, gp.path)
333 util.set_exec(dst, x)
334 util.set_exec(dst, x)
334 cmdutil.addremove(repo, cfiles, wlock=wlock)
335 cmdutil.addremove(repo, cfiles, wlock=wlock)
335 files = patches.keys()
336 files = patches.keys()
336 files.extend([r for r in removes if r not in files])
337 files.extend([r for r in removes if r not in files])
337 files.sort()
338 files.sort()
338
339
339 return files
340 return files
340
341
341 def diff(repo, node1=None, node2=None, files=None, match=util.always,
342 def diff(repo, node1=None, node2=None, files=None, match=util.always,
342 fp=None, changes=None, opts=None):
343 fp=None, changes=None, opts=None):
343 '''print diff of changes to files between two nodes, or node and
344 '''print diff of changes to files between two nodes, or node and
344 working directory.
345 working directory.
345
346
346 if node1 is None, use first dirstate parent instead.
347 if node1 is None, use first dirstate parent instead.
347 if node2 is None, compare node1 with working directory.'''
348 if node2 is None, compare node1 with working directory.'''
348
349
349 if opts is None:
350 if opts is None:
350 opts = mdiff.defaultopts
351 opts = mdiff.defaultopts
351 if fp is None:
352 if fp is None:
352 fp = repo.ui
353 fp = repo.ui
353
354
354 if not node1:
355 if not node1:
355 node1 = repo.dirstate.parents()[0]
356 node1 = repo.dirstate.parents()[0]
356
357
357 clcache = {}
358 clcache = {}
358 def getchangelog(n):
359 def getchangelog(n):
359 if n not in clcache:
360 if n not in clcache:
360 clcache[n] = repo.changelog.read(n)
361 clcache[n] = repo.changelog.read(n)
361 return clcache[n]
362 return clcache[n]
362 mcache = {}
363 mcache = {}
363 def getmanifest(n):
364 def getmanifest(n):
364 if n not in mcache:
365 if n not in mcache:
365 mcache[n] = repo.manifest.read(n)
366 mcache[n] = repo.manifest.read(n)
366 return mcache[n]
367 return mcache[n]
367 fcache = {}
368 fcache = {}
368 def getfile(f):
369 def getfile(f):
369 if f not in fcache:
370 if f not in fcache:
370 fcache[f] = repo.file(f)
371 fcache[f] = repo.file(f)
371 return fcache[f]
372 return fcache[f]
372
373
373 # reading the data for node1 early allows it to play nicely
374 # reading the data for node1 early allows it to play nicely
374 # with repo.status and the revlog cache.
375 # with repo.status and the revlog cache.
375 change = getchangelog(node1)
376 change = getchangelog(node1)
376 mmap = getmanifest(change[0])
377 mmap = getmanifest(change[0])
377 date1 = util.datestr(change[2])
378 date1 = util.datestr(change[2])
378
379
379 if not changes:
380 if not changes:
380 changes = repo.status(node1, node2, files, match=match)[:5]
381 changes = repo.status(node1, node2, files, match=match)[:5]
381 modified, added, removed, deleted, unknown = changes
382 modified, added, removed, deleted, unknown = changes
382 if files:
383 if files:
383 def filterfiles(filters):
384 def filterfiles(filters):
384 l = [x for x in filters if x in files]
385 l = [x for x in filters if x in files]
385
386
386 for t in files:
387 for t in files:
387 if not t.endswith("/"):
388 if not t.endswith("/"):
388 t += "/"
389 t += "/"
389 l += [x for x in filters if x.startswith(t)]
390 l += [x for x in filters if x.startswith(t)]
390 return l
391 return l
391
392
392 modified, added, removed = map(filterfiles, (modified, added, removed))
393 modified, added, removed = map(filterfiles, (modified, added, removed))
393
394
394 if not modified and not added and not removed:
395 if not modified and not added and not removed:
395 return
396 return
396
397
397 def renamedbetween(f, n1, n2):
398 def renamedbetween(f, n1, n2):
398 r1, r2 = map(repo.changelog.rev, (n1, n2))
399 r1, r2 = map(repo.changelog.rev, (n1, n2))
399 src = None
400 src = None
400 while r2 > r1:
401 while r2 > r1:
401 cl = getchangelog(n2)[0]
402 cl = getchangelog(n2)[0]
402 m = getmanifest(cl)
403 m = getmanifest(cl)
403 try:
404 try:
404 src = getfile(f).renamed(m[f])
405 src = getfile(f).renamed(m[f])
405 except KeyError:
406 except KeyError:
406 return None
407 return None
407 if src:
408 if src:
408 f = src[0]
409 f = src[0]
409 n2 = repo.changelog.parents(n2)[0]
410 n2 = repo.changelog.parents(n2)[0]
410 r2 = repo.changelog.rev(n2)
411 r2 = repo.changelog.rev(n2)
411 return src
412 return src
412
413
413 if node2:
414 if node2:
414 change = getchangelog(node2)
415 change = getchangelog(node2)
415 mmap2 = getmanifest(change[0])
416 mmap2 = getmanifest(change[0])
416 _date2 = util.datestr(change[2])
417 _date2 = util.datestr(change[2])
417 def date2(f):
418 def date2(f):
418 return _date2
419 return _date2
419 def read(f):
420 def read(f):
420 return getfile(f).read(mmap2[f])
421 return getfile(f).read(mmap2[f])
421 def renamed(f):
422 def renamed(f):
422 return renamedbetween(f, node1, node2)
423 return renamedbetween(f, node1, node2)
423 else:
424 else:
424 tz = util.makedate()[1]
425 tz = util.makedate()[1]
425 _date2 = util.datestr()
426 _date2 = util.datestr()
426 def date2(f):
427 def date2(f):
427 try:
428 try:
428 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
429 return util.datestr((os.lstat(repo.wjoin(f)).st_mtime, tz))
429 except OSError, err:
430 except OSError, err:
430 if err.errno != errno.ENOENT: raise
431 if err.errno != errno.ENOENT: raise
431 return _date2
432 return _date2
432 def read(f):
433 def read(f):
433 return repo.wread(f)
434 return repo.wread(f)
434 def renamed(f):
435 def renamed(f):
435 src = repo.dirstate.copies.get(f)
436 src = repo.dirstate.copies.get(f)
436 parent = repo.dirstate.parents()[0]
437 parent = repo.dirstate.parents()[0]
437 if src:
438 if src:
438 f = src[0]
439 f = src[0]
439 of = renamedbetween(f, node1, parent)
440 of = renamedbetween(f, node1, parent)
440 if of:
441 if of:
441 return of
442 return of
442 elif src:
443 elif src:
443 cl = getchangelog(parent)[0]
444 cl = getchangelog(parent)[0]
444 return (src, getmanifest(cl)[src])
445 return (src, getmanifest(cl)[src])
445 else:
446 else:
446 return None
447 return None
447
448
448 if repo.ui.quiet:
449 if repo.ui.quiet:
449 r = None
450 r = None
450 else:
451 else:
451 hexfunc = repo.ui.verbose and hex or short
452 hexfunc = repo.ui.verbose and hex or short
452 r = [hexfunc(node) for node in [node1, node2] if node]
453 r = [hexfunc(node) for node in [node1, node2] if node]
453
454
454 if opts.git:
455 if opts.git:
455 copied = {}
456 copied = {}
456 for f in added:
457 for f in added:
457 src = renamed(f)
458 src = renamed(f)
458 if src:
459 if src:
459 copied[f] = src
460 copied[f] = src
460 srcs = [x[1][0] for x in copied.items()]
461 srcs = [x[1][0] for x in copied.items()]
461
462
462 all = modified + added + removed
463 all = modified + added + removed
463 all.sort()
464 all.sort()
464 for f in all:
465 for f in all:
465 to = None
466 to = None
466 tn = None
467 tn = None
467 dodiff = True
468 dodiff = True
468 if f in mmap:
469 if f in mmap:
469 to = getfile(f).read(mmap[f])
470 to = getfile(f).read(mmap[f])
470 if f not in removed:
471 if f not in removed:
471 tn = read(f)
472 tn = read(f)
472 if opts.git:
473 if opts.git:
473 def gitmode(x):
474 def gitmode(x):
474 return x and '100755' or '100644'
475 return x and '100755' or '100644'
475 def addmodehdr(header, omode, nmode):
476 def addmodehdr(header, omode, nmode):
476 if omode != nmode:
477 if omode != nmode:
477 header.append('old mode %s\n' % omode)
478 header.append('old mode %s\n' % omode)
478 header.append('new mode %s\n' % nmode)
479 header.append('new mode %s\n' % nmode)
479
480
480 a, b = f, f
481 a, b = f, f
481 header = []
482 header = []
482 if f in added:
483 if f in added:
483 if node2:
484 if node2:
484 mode = gitmode(mmap2.execf(f))
485 mode = gitmode(mmap2.execf(f))
485 else:
486 else:
486 mode = gitmode(util.is_exec(repo.wjoin(f), None))
487 mode = gitmode(util.is_exec(repo.wjoin(f), None))
487 if f in copied:
488 if f in copied:
488 a, arev = copied[f]
489 a, arev = copied[f]
489 omode = gitmode(mmap.execf(a))
490 omode = gitmode(mmap.execf(a))
490 addmodehdr(header, omode, mode)
491 addmodehdr(header, omode, mode)
491 op = a in removed and 'rename' or 'copy'
492 op = a in removed and 'rename' or 'copy'
492 header.append('%s from %s\n' % (op, a))
493 header.append('%s from %s\n' % (op, a))
493 header.append('%s to %s\n' % (op, f))
494 header.append('%s to %s\n' % (op, f))
494 to = getfile(a).read(arev)
495 to = getfile(a).read(arev)
495 else:
496 else:
496 header.append('new file mode %s\n' % mode)
497 header.append('new file mode %s\n' % mode)
497 elif f in removed:
498 elif f in removed:
498 if f in srcs:
499 if f in srcs:
499 dodiff = False
500 dodiff = False
500 else:
501 else:
501 mode = gitmode(mmap.execf(f))
502 mode = gitmode(mmap.execf(f))
502 header.append('deleted file mode %s\n' % mode)
503 header.append('deleted file mode %s\n' % mode)
503 else:
504 else:
504 omode = gitmode(mmap.execf(f))
505 omode = gitmode(mmap.execf(f))
505 if node2:
506 if node2:
506 nmode = gitmode(mmap2.execf(f))
507 nmode = gitmode(mmap2.execf(f))
507 else:
508 else:
508 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
509 nmode = gitmode(util.is_exec(repo.wjoin(f), mmap.execf(f)))
509 addmodehdr(header, omode, nmode)
510 addmodehdr(header, omode, nmode)
510 r = None
511 r = None
511 if dodiff:
512 if dodiff:
512 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
513 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
513 fp.write(''.join(header))
514 fp.write(''.join(header))
514 if dodiff:
515 if dodiff:
515 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
516 fp.write(mdiff.unidiff(to, date1, tn, date2(f), f, r, opts=opts))
516
517
517 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
518 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
518 opts=None):
519 opts=None):
519 '''export changesets as hg patches.'''
520 '''export changesets as hg patches.'''
520
521
521 total = len(revs)
522 total = len(revs)
522 revwidth = max(map(len, revs))
523 revwidth = max(map(len, revs))
523
524
524 def single(node, seqno, fp):
525 def single(node, seqno, fp):
525 parents = [p for p in repo.changelog.parents(node) if p != nullid]
526 parents = [p for p in repo.changelog.parents(node) if p != nullid]
526 if switch_parent:
527 if switch_parent:
527 parents.reverse()
528 parents.reverse()
528 prev = (parents and parents[0]) or nullid
529 prev = (parents and parents[0]) or nullid
529 change = repo.changelog.read(node)
530 change = repo.changelog.read(node)
530
531
531 if not fp:
532 if not fp:
532 fp = cmdutil.make_file(repo, template, node, total=total,
533 fp = cmdutil.make_file(repo, template, node, total=total,
533 seqno=seqno, revwidth=revwidth)
534 seqno=seqno, revwidth=revwidth)
534 if fp not in (sys.stdout, repo.ui):
535 if fp not in (sys.stdout, repo.ui):
535 repo.ui.note("%s\n" % fp.name)
536 repo.ui.note("%s\n" % fp.name)
536
537
537 fp.write("# HG changeset patch\n")
538 fp.write("# HG changeset patch\n")
538 fp.write("# User %s\n" % change[1])
539 fp.write("# User %s\n" % change[1])
539 fp.write("# Date %d %d\n" % change[2])
540 fp.write("# Date %d %d\n" % change[2])
540 fp.write("# Node ID %s\n" % hex(node))
541 fp.write("# Node ID %s\n" % hex(node))
541 fp.write("# Parent %s\n" % hex(prev))
542 fp.write("# Parent %s\n" % hex(prev))
542 if len(parents) > 1:
543 if len(parents) > 1:
543 fp.write("# Parent %s\n" % hex(parents[1]))
544 fp.write("# Parent %s\n" % hex(parents[1]))
544 fp.write(change[4].rstrip())
545 fp.write(change[4].rstrip())
545 fp.write("\n\n")
546 fp.write("\n\n")
546
547
547 diff(repo, prev, node, fp=fp, opts=opts)
548 diff(repo, prev, node, fp=fp, opts=opts)
548 if fp not in (sys.stdout, repo.ui):
549 if fp not in (sys.stdout, repo.ui):
549 fp.close()
550 fp.close()
550
551
551 for seqno, cset in enumerate(revs):
552 for seqno, cset in enumerate(revs):
552 single(cset, seqno, fp)
553 single(cset, seqno, fp)
554
555 def diffstat(patchlines):
556 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
557 try:
558 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
559 try:
560 for line in patchlines: print >> p.tochild, line
561 p.tochild.close()
562 if p.wait(): return
563 fp = os.fdopen(fd, 'r')
564 stat = []
565 for line in fp: stat.append(line.lstrip())
566 last = stat.pop()
567 stat.insert(0, last)
568 stat = ''.join(stat)
569 if stat.startswith('0 files'): raise ValueError
570 return stat
571 except: raise
572 finally:
573 try: os.unlink(name)
574 except: pass
General Comments 0
You need to be logged in to leave comments. Login now