##// END OF EJS Templates
help: remove per-extension paragraph on how to enable it...
Cédric Duval -
r8866:87c30fb7 default
parent child Browse files
Show More
@@ -1,270 +1,266 b''
1 # color.py color output for the status and qseries commands
1 # color.py color output for the status and qseries commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 #
4 #
5 # This program is free software; you can redistribute it and/or modify it
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation; either version 2 of the License, or (at your
7 # Free Software Foundation; either version 2 of the License, or (at your
8 # option) any later version.
8 # option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful, but
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 # Public License for more details.
13 # Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License along
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
19 '''add color output to status, qseries, and diff-related commands
19 '''add color output to status, qseries, and diff-related commands
20
20
21 This extension modifies the status command to add color to its output
21 This extension modifies the status command to add color to its output
22 to reflect file status, the qseries command to add color to reflect
22 to reflect file status, the qseries command to add color to reflect
23 patch status (applied, unapplied, missing), and to diff-related
23 patch status (applied, unapplied, missing), and to diff-related
24 commands to highlight additions, removals, diff headers, and trailing
24 commands to highlight additions, removals, diff headers, and trailing
25 whitespace.
25 whitespace.
26
26
27 Other effects in addition to color, like bold and underlined text, are
27 Other effects in addition to color, like bold and underlined text, are
28 also available. Effects are rendered with the ECMA-48 SGR control
28 also available. Effects are rendered with the ECMA-48 SGR control
29 function (aka ANSI escape codes). This module also provides the
29 function (aka ANSI escape codes). This module also provides the
30 render_text function, which can be used to add effects to any text.
30 render_text function, which can be used to add effects to any text.
31
31
32 To enable this extension, add this to your .hgrc file:
33 [extensions]
34 color =
35
36 Default effects may be overridden from the .hgrc file:
32 Default effects may be overridden from the .hgrc file:
37
33
38 [color]
34 [color]
39 status.modified = blue bold underline red_background
35 status.modified = blue bold underline red_background
40 status.added = green bold
36 status.added = green bold
41 status.removed = red bold blue_background
37 status.removed = red bold blue_background
42 status.deleted = cyan bold underline
38 status.deleted = cyan bold underline
43 status.unknown = magenta bold underline
39 status.unknown = magenta bold underline
44 status.ignored = black bold
40 status.ignored = black bold
45
41
46 # 'none' turns off all effects
42 # 'none' turns off all effects
47 status.clean = none
43 status.clean = none
48 status.copied = none
44 status.copied = none
49
45
50 qseries.applied = blue bold underline
46 qseries.applied = blue bold underline
51 qseries.unapplied = black bold
47 qseries.unapplied = black bold
52 qseries.missing = red bold
48 qseries.missing = red bold
53
49
54 diff.diffline = bold
50 diff.diffline = bold
55 diff.extended = cyan bold
51 diff.extended = cyan bold
56 diff.file_a = red bold
52 diff.file_a = red bold
57 diff.file_b = green bold
53 diff.file_b = green bold
58 diff.hunk = magenta
54 diff.hunk = magenta
59 diff.deleted = red
55 diff.deleted = red
60 diff.inserted = green
56 diff.inserted = green
61 diff.changed = white
57 diff.changed = white
62 diff.trailingwhitespace = bold red_background
58 diff.trailingwhitespace = bold red_background
63 '''
59 '''
64
60
65 import os, sys
61 import os, sys
66
62
67 from mercurial import cmdutil, commands, extensions
63 from mercurial import cmdutil, commands, extensions
68 from mercurial.i18n import _
64 from mercurial.i18n import _
69
65
70 # start and stop parameters for effects
66 # start and stop parameters for effects
71 _effect_params = {'none': 0,
67 _effect_params = {'none': 0,
72 'black': 30,
68 'black': 30,
73 'red': 31,
69 'red': 31,
74 'green': 32,
70 'green': 32,
75 'yellow': 33,
71 'yellow': 33,
76 'blue': 34,
72 'blue': 34,
77 'magenta': 35,
73 'magenta': 35,
78 'cyan': 36,
74 'cyan': 36,
79 'white': 37,
75 'white': 37,
80 'bold': 1,
76 'bold': 1,
81 'italic': 3,
77 'italic': 3,
82 'underline': 4,
78 'underline': 4,
83 'inverse': 7,
79 'inverse': 7,
84 'black_background': 40,
80 'black_background': 40,
85 'red_background': 41,
81 'red_background': 41,
86 'green_background': 42,
82 'green_background': 42,
87 'yellow_background': 43,
83 'yellow_background': 43,
88 'blue_background': 44,
84 'blue_background': 44,
89 'purple_background': 45,
85 'purple_background': 45,
90 'cyan_background': 46,
86 'cyan_background': 46,
91 'white_background': 47}
87 'white_background': 47}
92
88
93 def render_effects(text, effects):
89 def render_effects(text, effects):
94 'Wrap text in commands to turn on each effect.'
90 'Wrap text in commands to turn on each effect.'
95 start = [str(_effect_params[e]) for e in ['none'] + effects]
91 start = [str(_effect_params[e]) for e in ['none'] + effects]
96 start = '\033[' + ';'.join(start) + 'm'
92 start = '\033[' + ';'.join(start) + 'm'
97 stop = '\033[' + str(_effect_params['none']) + 'm'
93 stop = '\033[' + str(_effect_params['none']) + 'm'
98 return ''.join([start, text, stop])
94 return ''.join([start, text, stop])
99
95
100 def colorstatus(orig, ui, repo, *pats, **opts):
96 def colorstatus(orig, ui, repo, *pats, **opts):
101 '''run the status command with colored output'''
97 '''run the status command with colored output'''
102
98
103 delimiter = opts['print0'] and '\0' or '\n'
99 delimiter = opts['print0'] and '\0' or '\n'
104
100
105 nostatus = opts.get('no_status')
101 nostatus = opts.get('no_status')
106 opts['no_status'] = False
102 opts['no_status'] = False
107 # run status and capture its output
103 # run status and capture its output
108 ui.pushbuffer()
104 ui.pushbuffer()
109 retval = orig(ui, repo, *pats, **opts)
105 retval = orig(ui, repo, *pats, **opts)
110 # filter out empty strings
106 # filter out empty strings
111 lines_with_status = [ line for line in ui.popbuffer().split(delimiter) if line ]
107 lines_with_status = [ line for line in ui.popbuffer().split(delimiter) if line ]
112
108
113 if nostatus:
109 if nostatus:
114 lines = [l[2:] for l in lines_with_status]
110 lines = [l[2:] for l in lines_with_status]
115 else:
111 else:
116 lines = lines_with_status
112 lines = lines_with_status
117
113
118 # apply color to output and display it
114 # apply color to output and display it
119 for i in xrange(len(lines)):
115 for i in xrange(len(lines)):
120 status = _status_abbreviations[lines_with_status[i][0]]
116 status = _status_abbreviations[lines_with_status[i][0]]
121 effects = _status_effects[status]
117 effects = _status_effects[status]
122 if effects:
118 if effects:
123 lines[i] = render_effects(lines[i], effects)
119 lines[i] = render_effects(lines[i], effects)
124 ui.write(lines[i] + delimiter)
120 ui.write(lines[i] + delimiter)
125 return retval
121 return retval
126
122
127 _status_abbreviations = { 'M': 'modified',
123 _status_abbreviations = { 'M': 'modified',
128 'A': 'added',
124 'A': 'added',
129 'R': 'removed',
125 'R': 'removed',
130 '!': 'deleted',
126 '!': 'deleted',
131 '?': 'unknown',
127 '?': 'unknown',
132 'I': 'ignored',
128 'I': 'ignored',
133 'C': 'clean',
129 'C': 'clean',
134 ' ': 'copied', }
130 ' ': 'copied', }
135
131
136 _status_effects = { 'modified': ['blue', 'bold'],
132 _status_effects = { 'modified': ['blue', 'bold'],
137 'added': ['green', 'bold'],
133 'added': ['green', 'bold'],
138 'removed': ['red', 'bold'],
134 'removed': ['red', 'bold'],
139 'deleted': ['cyan', 'bold', 'underline'],
135 'deleted': ['cyan', 'bold', 'underline'],
140 'unknown': ['magenta', 'bold', 'underline'],
136 'unknown': ['magenta', 'bold', 'underline'],
141 'ignored': ['black', 'bold'],
137 'ignored': ['black', 'bold'],
142 'clean': ['none'],
138 'clean': ['none'],
143 'copied': ['none'], }
139 'copied': ['none'], }
144
140
145 def colorqseries(orig, ui, repo, *dummy, **opts):
141 def colorqseries(orig, ui, repo, *dummy, **opts):
146 '''run the qseries command with colored output'''
142 '''run the qseries command with colored output'''
147 ui.pushbuffer()
143 ui.pushbuffer()
148 retval = orig(ui, repo, **opts)
144 retval = orig(ui, repo, **opts)
149 patches = ui.popbuffer().splitlines()
145 patches = ui.popbuffer().splitlines()
150 for patch in patches:
146 for patch in patches:
151 patchname = patch
147 patchname = patch
152 if opts['summary']:
148 if opts['summary']:
153 patchname = patchname.split(': ')[0]
149 patchname = patchname.split(': ')[0]
154 if ui.verbose:
150 if ui.verbose:
155 patchname = patchname.split(' ', 2)[-1]
151 patchname = patchname.split(' ', 2)[-1]
156
152
157 if opts['missing']:
153 if opts['missing']:
158 effects = _patch_effects['missing']
154 effects = _patch_effects['missing']
159 # Determine if patch is applied.
155 # Determine if patch is applied.
160 elif [ applied for applied in repo.mq.applied
156 elif [ applied for applied in repo.mq.applied
161 if patchname == applied.name ]:
157 if patchname == applied.name ]:
162 effects = _patch_effects['applied']
158 effects = _patch_effects['applied']
163 else:
159 else:
164 effects = _patch_effects['unapplied']
160 effects = _patch_effects['unapplied']
165 ui.write(render_effects(patch, effects) + '\n')
161 ui.write(render_effects(patch, effects) + '\n')
166 return retval
162 return retval
167
163
168 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
164 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
169 'missing': ['red', 'bold'],
165 'missing': ['red', 'bold'],
170 'unapplied': ['black', 'bold'], }
166 'unapplied': ['black', 'bold'], }
171
167
172 def colorwrap(orig, s):
168 def colorwrap(orig, s):
173 '''wrap ui.write for colored diff output'''
169 '''wrap ui.write for colored diff output'''
174 lines = s.split('\n')
170 lines = s.split('\n')
175 for i, line in enumerate(lines):
171 for i, line in enumerate(lines):
176 stripline = line
172 stripline = line
177 if line and line[0] in '+-':
173 if line and line[0] in '+-':
178 # highlight trailing whitespace, but only in changed lines
174 # highlight trailing whitespace, but only in changed lines
179 stripline = line.rstrip()
175 stripline = line.rstrip()
180 for prefix, style in _diff_prefixes:
176 for prefix, style in _diff_prefixes:
181 if stripline.startswith(prefix):
177 if stripline.startswith(prefix):
182 lines[i] = render_effects(stripline, _diff_effects[style])
178 lines[i] = render_effects(stripline, _diff_effects[style])
183 break
179 break
184 if line != stripline:
180 if line != stripline:
185 lines[i] += render_effects(
181 lines[i] += render_effects(
186 line[len(stripline):], _diff_effects['trailingwhitespace'])
182 line[len(stripline):], _diff_effects['trailingwhitespace'])
187 orig('\n'.join(lines))
183 orig('\n'.join(lines))
188
184
189 def colorshowpatch(orig, self, node):
185 def colorshowpatch(orig, self, node):
190 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
186 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
191 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
187 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
192 try:
188 try:
193 orig(self, node)
189 orig(self, node)
194 finally:
190 finally:
195 self.ui.write = oldwrite
191 self.ui.write = oldwrite
196
192
197 def colordiff(orig, ui, repo, *pats, **opts):
193 def colordiff(orig, ui, repo, *pats, **opts):
198 '''run the diff command with colored output'''
194 '''run the diff command with colored output'''
199 oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
195 oldwrite = extensions.wrapfunction(ui, 'write', colorwrap)
200 try:
196 try:
201 orig(ui, repo, *pats, **opts)
197 orig(ui, repo, *pats, **opts)
202 finally:
198 finally:
203 ui.write = oldwrite
199 ui.write = oldwrite
204
200
205 _diff_prefixes = [('diff', 'diffline'),
201 _diff_prefixes = [('diff', 'diffline'),
206 ('copy', 'extended'),
202 ('copy', 'extended'),
207 ('rename', 'extended'),
203 ('rename', 'extended'),
208 ('old', 'extended'),
204 ('old', 'extended'),
209 ('new', 'extended'),
205 ('new', 'extended'),
210 ('deleted', 'extended'),
206 ('deleted', 'extended'),
211 ('---', 'file_a'),
207 ('---', 'file_a'),
212 ('+++', 'file_b'),
208 ('+++', 'file_b'),
213 ('@', 'hunk'),
209 ('@', 'hunk'),
214 ('-', 'deleted'),
210 ('-', 'deleted'),
215 ('+', 'inserted')]
211 ('+', 'inserted')]
216
212
217 _diff_effects = {'diffline': ['bold'],
213 _diff_effects = {'diffline': ['bold'],
218 'extended': ['cyan', 'bold'],
214 'extended': ['cyan', 'bold'],
219 'file_a': ['red', 'bold'],
215 'file_a': ['red', 'bold'],
220 'file_b': ['green', 'bold'],
216 'file_b': ['green', 'bold'],
221 'hunk': ['magenta'],
217 'hunk': ['magenta'],
222 'deleted': ['red'],
218 'deleted': ['red'],
223 'inserted': ['green'],
219 'inserted': ['green'],
224 'changed': ['white'],
220 'changed': ['white'],
225 'trailingwhitespace': ['bold', 'red_background']}
221 'trailingwhitespace': ['bold', 'red_background']}
226
222
227 def uisetup(ui):
223 def uisetup(ui):
228 '''Initialize the extension.'''
224 '''Initialize the extension.'''
229 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
225 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
230 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
226 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
231 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
227 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
232 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
228 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
233 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
229 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
234 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
230 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
235 try:
231 try:
236 mq = extensions.find('mq')
232 mq = extensions.find('mq')
237 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
233 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
238 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
234 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
239 except KeyError:
235 except KeyError:
240 # The mq extension is not enabled
236 # The mq extension is not enabled
241 pass
237 pass
242
238
243 def _setupcmd(ui, cmd, table, func, effectsmap):
239 def _setupcmd(ui, cmd, table, func, effectsmap):
244 '''patch in command to command table and load effect map'''
240 '''patch in command to command table and load effect map'''
245 def nocolor(orig, *args, **opts):
241 def nocolor(orig, *args, **opts):
246
242
247 if (opts['no_color'] or opts['color'] == 'never' or
243 if (opts['no_color'] or opts['color'] == 'never' or
248 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
244 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
249 or not sys.__stdout__.isatty()))):
245 or not sys.__stdout__.isatty()))):
250 return orig(*args, **opts)
246 return orig(*args, **opts)
251
247
252 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
248 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
253 'showpatch', colorshowpatch)
249 'showpatch', colorshowpatch)
254 try:
250 try:
255 if func is not None:
251 if func is not None:
256 return func(orig, *args, **opts)
252 return func(orig, *args, **opts)
257 return orig(*args, **opts)
253 return orig(*args, **opts)
258 finally:
254 finally:
259 cmdutil.changeset_printer.showpatch = oldshowpatch
255 cmdutil.changeset_printer.showpatch = oldshowpatch
260
256
261 entry = extensions.wrapcommand(table, cmd, nocolor)
257 entry = extensions.wrapcommand(table, cmd, nocolor)
262 entry[1].extend([
258 entry[1].extend([
263 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
259 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
264 ('', 'no-color', None, _("don't colorize output")),
260 ('', 'no-color', None, _("don't colorize output")),
265 ])
261 ])
266
262
267 for status in effectsmap:
263 for status in effectsmap:
268 effects = ui.configlist('color', cmd + '.' + status)
264 effects = ui.configlist('color', cmd + '.' + status)
269 if effects:
265 if effects:
270 effectsmap[status] = effects
266 effectsmap[status] = effects
@@ -1,233 +1,228 b''
1 # extdiff.py - external diff program support for mercurial
1 # extdiff.py - external diff program support 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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''
8 '''
9 The `extdiff' Mercurial extension allows you to use external programs
9 The `extdiff' Mercurial extension allows you to use external programs
10 to compare revisions, or revision with working directory. The external diff
10 to compare revisions, or revision with working directory. The external diff
11 programs are called with a configurable set of options and two
11 programs are called with a configurable set of options and two
12 non-option arguments: paths to directories containing snapshots of
12 non-option arguments: paths to directories containing snapshots of
13 files to compare.
13 files to compare.
14
14
15 To enable this extension:
16
17 [extensions]
18 hgext.extdiff =
19
20 The `extdiff' extension also allows to configure new diff commands, so
15 The `extdiff' extension also allows to configure new diff commands, so
21 you do not need to type "hg extdiff -p kdiff3" always.
16 you do not need to type "hg extdiff -p kdiff3" always.
22
17
23 [extdiff]
18 [extdiff]
24 # add new command that runs GNU diff(1) in 'context diff' mode
19 # add new command that runs GNU diff(1) in 'context diff' mode
25 cdiff = gdiff -Nprc5
20 cdiff = gdiff -Nprc5
26 ## or the old way:
21 ## or the old way:
27 #cmd.cdiff = gdiff
22 #cmd.cdiff = gdiff
28 #opts.cdiff = -Nprc5
23 #opts.cdiff = -Nprc5
29
24
30 # add new command called vdiff, runs kdiff3
25 # add new command called vdiff, runs kdiff3
31 vdiff = kdiff3
26 vdiff = kdiff3
32
27
33 # add new command called meld, runs meld (no need to name twice)
28 # add new command called meld, runs meld (no need to name twice)
34 meld =
29 meld =
35
30
36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
31 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
37 # (see http://www.vim.org/scripts/script.php?script_id=102)
32 # (see http://www.vim.org/scripts/script.php?script_id=102)
38 # Non English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
33 # Non English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
39 # your .vimrc
34 # your .vimrc
40 vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
35 vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
41
36
42 You can use -I/-X and list of file or directory names like normal "hg
37 You can use -I/-X and list of file or directory names like normal "hg
43 diff" command. The `extdiff' extension makes snapshots of only needed
38 diff" command. The `extdiff' extension makes snapshots of only needed
44 files, so running the external diff program will actually be pretty
39 files, so running the external diff program will actually be pretty
45 fast (at least faster than having to compare the entire tree).
40 fast (at least faster than having to compare the entire tree).
46 '''
41 '''
47
42
48 from mercurial.i18n import _
43 from mercurial.i18n import _
49 from mercurial.node import short
44 from mercurial.node import short
50 from mercurial import cmdutil, util, commands
45 from mercurial import cmdutil, util, commands
51 import os, shlex, shutil, tempfile
46 import os, shlex, shutil, tempfile
52
47
53 def snapshot(ui, repo, files, node, tmproot):
48 def snapshot(ui, repo, files, node, tmproot):
54 '''snapshot files as of some revision
49 '''snapshot files as of some revision
55 if not using snapshot, -I/-X does not work and recursive diff
50 if not using snapshot, -I/-X does not work and recursive diff
56 in tools like kdiff3 and meld displays too many files.'''
51 in tools like kdiff3 and meld displays too many files.'''
57 dirname = os.path.basename(repo.root)
52 dirname = os.path.basename(repo.root)
58 if dirname == "":
53 if dirname == "":
59 dirname = "root"
54 dirname = "root"
60 if node is not None:
55 if node is not None:
61 dirname = '%s.%s' % (dirname, short(node))
56 dirname = '%s.%s' % (dirname, short(node))
62 base = os.path.join(tmproot, dirname)
57 base = os.path.join(tmproot, dirname)
63 os.mkdir(base)
58 os.mkdir(base)
64 if node is not None:
59 if node is not None:
65 ui.note(_('making snapshot of %d files from rev %s\n') %
60 ui.note(_('making snapshot of %d files from rev %s\n') %
66 (len(files), short(node)))
61 (len(files), short(node)))
67 else:
62 else:
68 ui.note(_('making snapshot of %d files from working directory\n') %
63 ui.note(_('making snapshot of %d files from working directory\n') %
69 (len(files)))
64 (len(files)))
70 wopener = util.opener(base)
65 wopener = util.opener(base)
71 fns_and_mtime = []
66 fns_and_mtime = []
72 ctx = repo[node]
67 ctx = repo[node]
73 for fn in files:
68 for fn in files:
74 wfn = util.pconvert(fn)
69 wfn = util.pconvert(fn)
75 if not wfn in ctx:
70 if not wfn in ctx:
76 # skipping new file after a merge ?
71 # skipping new file after a merge ?
77 continue
72 continue
78 ui.note(' %s\n' % wfn)
73 ui.note(' %s\n' % wfn)
79 dest = os.path.join(base, wfn)
74 dest = os.path.join(base, wfn)
80 fctx = ctx[wfn]
75 fctx = ctx[wfn]
81 data = repo.wwritedata(wfn, fctx.data())
76 data = repo.wwritedata(wfn, fctx.data())
82 if 'l' in fctx.flags():
77 if 'l' in fctx.flags():
83 wopener.symlink(data, wfn)
78 wopener.symlink(data, wfn)
84 else:
79 else:
85 wopener(wfn, 'w').write(data)
80 wopener(wfn, 'w').write(data)
86 if 'x' in fctx.flags():
81 if 'x' in fctx.flags():
87 util.set_flags(dest, False, True)
82 util.set_flags(dest, False, True)
88 if node is None:
83 if node is None:
89 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
84 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
90 return dirname, fns_and_mtime
85 return dirname, fns_and_mtime
91
86
92 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
87 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
93 '''Do the actuall diff:
88 '''Do the actuall diff:
94
89
95 - copy to a temp structure if diffing 2 internal revisions
90 - copy to a temp structure if diffing 2 internal revisions
96 - copy to a temp structure if diffing working revision with
91 - copy to a temp structure if diffing working revision with
97 another one and more than 1 file is changed
92 another one and more than 1 file is changed
98 - just invoke the diff for a single file in the working dir
93 - just invoke the diff for a single file in the working dir
99 '''
94 '''
100
95
101 revs = opts.get('rev')
96 revs = opts.get('rev')
102 change = opts.get('change')
97 change = opts.get('change')
103
98
104 if revs and change:
99 if revs and change:
105 msg = _('cannot specify --rev and --change at the same time')
100 msg = _('cannot specify --rev and --change at the same time')
106 raise util.Abort(msg)
101 raise util.Abort(msg)
107 elif change:
102 elif change:
108 node2 = repo.lookup(change)
103 node2 = repo.lookup(change)
109 node1 = repo[node2].parents()[0].node()
104 node1 = repo[node2].parents()[0].node()
110 else:
105 else:
111 node1, node2 = cmdutil.revpair(repo, revs)
106 node1, node2 = cmdutil.revpair(repo, revs)
112
107
113 matcher = cmdutil.match(repo, pats, opts)
108 matcher = cmdutil.match(repo, pats, opts)
114 modified, added, removed = repo.status(node1, node2, matcher)[:3]
109 modified, added, removed = repo.status(node1, node2, matcher)[:3]
115 if not (modified or added or removed):
110 if not (modified or added or removed):
116 return 0
111 return 0
117
112
118 tmproot = tempfile.mkdtemp(prefix='extdiff.')
113 tmproot = tempfile.mkdtemp(prefix='extdiff.')
119 dir2root = ''
114 dir2root = ''
120 try:
115 try:
121 # Always make a copy of node1
116 # Always make a copy of node1
122 dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0]
117 dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0]
123 changes = len(modified) + len(removed) + len(added)
118 changes = len(modified) + len(removed) + len(added)
124
119
125 # If node2 in not the wc or there is >1 change, copy it
120 # If node2 in not the wc or there is >1 change, copy it
126 if node2 or changes > 1:
121 if node2 or changes > 1:
127 dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot)
122 dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot)
128 else:
123 else:
129 # This lets the diff tool open the changed file directly
124 # This lets the diff tool open the changed file directly
130 dir2 = ''
125 dir2 = ''
131 dir2root = repo.root
126 dir2root = repo.root
132 fns_and_mtime = []
127 fns_and_mtime = []
133
128
134 # If only one change, diff the files instead of the directories
129 # If only one change, diff the files instead of the directories
135 if changes == 1 :
130 if changes == 1 :
136 if len(modified):
131 if len(modified):
137 dir1 = os.path.join(dir1, util.localpath(modified[0]))
132 dir1 = os.path.join(dir1, util.localpath(modified[0]))
138 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
133 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
139 elif len(removed) :
134 elif len(removed) :
140 dir1 = os.path.join(dir1, util.localpath(removed[0]))
135 dir1 = os.path.join(dir1, util.localpath(removed[0]))
141 dir2 = os.devnull
136 dir2 = os.devnull
142 else:
137 else:
143 dir1 = os.devnull
138 dir1 = os.devnull
144 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
139 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
145
140
146 cmdline = ('%s %s %s %s' %
141 cmdline = ('%s %s %s %s' %
147 (util.shellquote(diffcmd), ' '.join(diffopts),
142 (util.shellquote(diffcmd), ' '.join(diffopts),
148 util.shellquote(dir1), util.shellquote(dir2)))
143 util.shellquote(dir1), util.shellquote(dir2)))
149 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
144 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
150 util.system(cmdline, cwd=tmproot)
145 util.system(cmdline, cwd=tmproot)
151
146
152 for copy_fn, working_fn, mtime in fns_and_mtime:
147 for copy_fn, working_fn, mtime in fns_and_mtime:
153 if os.path.getmtime(copy_fn) != mtime:
148 if os.path.getmtime(copy_fn) != mtime:
154 ui.debug(_('file changed while diffing. '
149 ui.debug(_('file changed while diffing. '
155 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
150 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
156 util.copyfile(copy_fn, working_fn)
151 util.copyfile(copy_fn, working_fn)
157
152
158 return 1
153 return 1
159 finally:
154 finally:
160 ui.note(_('cleaning up temp directory\n'))
155 ui.note(_('cleaning up temp directory\n'))
161 shutil.rmtree(tmproot)
156 shutil.rmtree(tmproot)
162
157
163 def extdiff(ui, repo, *pats, **opts):
158 def extdiff(ui, repo, *pats, **opts):
164 '''use external program to diff repository (or selected files)
159 '''use external program to diff repository (or selected files)
165
160
166 Show differences between revisions for the specified files, using
161 Show differences between revisions for the specified files, using
167 an external program. The default program used is diff, with
162 an external program. The default program used is diff, with
168 default options "-Npru".
163 default options "-Npru".
169
164
170 To select a different program, use the -p/--program option. The
165 To select a different program, use the -p/--program option. The
171 program will be passed the names of two directories to compare. To
166 program will be passed the names of two directories to compare. To
172 pass additional options to the program, use -o/--option. These
167 pass additional options to the program, use -o/--option. These
173 will be passed before the names of the directories to compare.
168 will be passed before the names of the directories to compare.
174
169
175 When two revision arguments are given, then changes are shown
170 When two revision arguments are given, then changes are shown
176 between those revisions. If only one revision is specified then
171 between those revisions. If only one revision is specified then
177 that revision is compared to the working directory, and, when no
172 that revision is compared to the working directory, and, when no
178 revisions are specified, the working directory files are compared
173 revisions are specified, the working directory files are compared
179 to its parent.'''
174 to its parent.'''
180 program = opts['program'] or 'diff'
175 program = opts['program'] or 'diff'
181 if opts['program']:
176 if opts['program']:
182 option = opts['option']
177 option = opts['option']
183 else:
178 else:
184 option = opts['option'] or ['-Npru']
179 option = opts['option'] or ['-Npru']
185 return dodiff(ui, repo, program, option, pats, opts)
180 return dodiff(ui, repo, program, option, pats, opts)
186
181
187 cmdtable = {
182 cmdtable = {
188 "extdiff":
183 "extdiff":
189 (extdiff,
184 (extdiff,
190 [('p', 'program', '', _('comparison program to run')),
185 [('p', 'program', '', _('comparison program to run')),
191 ('o', 'option', [], _('pass option to comparison program')),
186 ('o', 'option', [], _('pass option to comparison program')),
192 ('r', 'rev', [], _('revision')),
187 ('r', 'rev', [], _('revision')),
193 ('c', 'change', '', _('change made by revision')),
188 ('c', 'change', '', _('change made by revision')),
194 ] + commands.walkopts,
189 ] + commands.walkopts,
195 _('hg extdiff [OPT]... [FILE]...')),
190 _('hg extdiff [OPT]... [FILE]...')),
196 }
191 }
197
192
198 def uisetup(ui):
193 def uisetup(ui):
199 for cmd, path in ui.configitems('extdiff'):
194 for cmd, path in ui.configitems('extdiff'):
200 if cmd.startswith('cmd.'):
195 if cmd.startswith('cmd.'):
201 cmd = cmd[4:]
196 cmd = cmd[4:]
202 if not path: path = cmd
197 if not path: path = cmd
203 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
198 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
204 diffopts = diffopts and [diffopts] or []
199 diffopts = diffopts and [diffopts] or []
205 elif cmd.startswith('opts.'):
200 elif cmd.startswith('opts.'):
206 continue
201 continue
207 else:
202 else:
208 # command = path opts
203 # command = path opts
209 if path:
204 if path:
210 diffopts = shlex.split(path)
205 diffopts = shlex.split(path)
211 path = diffopts.pop(0)
206 path = diffopts.pop(0)
212 else:
207 else:
213 path, diffopts = cmd, []
208 path, diffopts = cmd, []
214 def save(cmd, path, diffopts):
209 def save(cmd, path, diffopts):
215 '''use closure to save diff command to use'''
210 '''use closure to save diff command to use'''
216 def mydiff(ui, repo, *pats, **opts):
211 def mydiff(ui, repo, *pats, **opts):
217 return dodiff(ui, repo, path, diffopts, pats, opts)
212 return dodiff(ui, repo, path, diffopts, pats, opts)
218 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
213 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
219
214
220 Show differences between revisions for the specified
215 Show differences between revisions for the specified
221 files, using the %(path)s program.
216 files, using the %(path)s program.
222
217
223 When two revision arguments are given, then changes are
218 When two revision arguments are given, then changes are
224 shown between those revisions. If only one revision is
219 shown between those revisions. If only one revision is
225 specified then that revision is compared to the working
220 specified then that revision is compared to the working
226 directory, and, when no revisions are specified, the
221 directory, and, when no revisions are specified, the
227 working directory files are compared to its parent.''' % {
222 working directory files are compared to its parent.''' % {
228 'path': util.uirepr(path),
223 'path': util.uirepr(path),
229 }
224 }
230 return mydiff
225 return mydiff
231 cmdtable[cmd] = (save(cmd, path, diffopts),
226 cmdtable[cmd] = (save(cmd, path, diffopts),
232 cmdtable['extdiff'][1][1:],
227 cmdtable['extdiff'][1][1:],
233 _('hg %s [OPTION]... [FILE]...') % cmd)
228 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,358 +1,346 b''
1 # Minimal support for git commands on an hg repository
1 # Minimal support for git commands on an hg repository
2 #
2 #
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''browsing the repository in a graphical way
8 '''browsing the repository in a graphical way
9
9
10 The hgk extension allows browsing the history of a repository in a
10 The hgk extension allows browsing the history of a repository in a
11 graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not
11 graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not
12 distributed with Mercurial.)
12 distributed with Mercurial.)
13
13
14 hgk consists of two parts: a Tcl script that does the displaying and
14 hgk consists of two parts: a Tcl script that does the displaying and
15 querying of information, and an extension to Mercurial named hgk.py,
15 querying of information, and an extension to Mercurial named hgk.py,
16 which provides hooks for hgk to get information. hgk can be found in
16 which provides hooks for hgk to get information. hgk can be found in
17 the contrib directory, and hgk.py can be found in the hgext directory.
17 the contrib directory, and the extension is shipped in the hgext
18
18 repository, and needs to be enabled.
19 To load the hgext.py extension, add it to your .hgrc file (you have to
20 use your global $HOME/.hgrc file, not one in a repository). You can
21 specify an absolute path:
22
23 [extensions]
24 hgk=/usr/local/lib/hgk.py
25
26 Mercurial can also scan the default python library path for a file
27 named 'hgk.py' if you set hgk empty:
28
29 [extensions]
30 hgk=
31
19
32 The hg view command will launch the hgk Tcl script. For this command
20 The hg view command will launch the hgk Tcl script. For this command
33 to work, hgk must be in your search path. Alternately, you can specify
21 to work, hgk must be in your search path. Alternately, you can specify
34 the path to hgk in your .hgrc file:
22 the path to hgk in your .hgrc file:
35
23
36 [hgk]
24 [hgk]
37 path=/location/of/hgk
25 path=/location/of/hgk
38
26
39 hgk can make use of the extdiff extension to visualize revisions.
27 hgk can make use of the extdiff extension to visualize revisions.
40 Assuming you had already configured extdiff vdiff command, just add:
28 Assuming you had already configured extdiff vdiff command, just add:
41
29
42 [hgk]
30 [hgk]
43 vdiff=vdiff
31 vdiff=vdiff
44
32
45 Revisions context menu will now display additional entries to fire
33 Revisions context menu will now display additional entries to fire
46 vdiff on hovered and selected revisions.'''
34 vdiff on hovered and selected revisions.'''
47
35
48 import os
36 import os
49 from mercurial import commands, util, patch, revlog, cmdutil
37 from mercurial import commands, util, patch, revlog, cmdutil
50 from mercurial.node import nullid, nullrev, short
38 from mercurial.node import nullid, nullrev, short
51 from mercurial.i18n import _
39 from mercurial.i18n import _
52
40
53 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
41 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
54 """diff trees from two commits"""
42 """diff trees from two commits"""
55 def __difftree(repo, node1, node2, files=[]):
43 def __difftree(repo, node1, node2, files=[]):
56 assert node2 is not None
44 assert node2 is not None
57 mmap = repo[node1].manifest()
45 mmap = repo[node1].manifest()
58 mmap2 = repo[node2].manifest()
46 mmap2 = repo[node2].manifest()
59 m = cmdutil.match(repo, files)
47 m = cmdutil.match(repo, files)
60 modified, added, removed = repo.status(node1, node2, m)[:3]
48 modified, added, removed = repo.status(node1, node2, m)[:3]
61 empty = short(nullid)
49 empty = short(nullid)
62
50
63 for f in modified:
51 for f in modified:
64 # TODO get file permissions
52 # TODO get file permissions
65 ui.write(":100664 100664 %s %s M\t%s\t%s\n" %
53 ui.write(":100664 100664 %s %s M\t%s\t%s\n" %
66 (short(mmap[f]), short(mmap2[f]), f, f))
54 (short(mmap[f]), short(mmap2[f]), f, f))
67 for f in added:
55 for f in added:
68 ui.write(":000000 100664 %s %s N\t%s\t%s\n" %
56 ui.write(":000000 100664 %s %s N\t%s\t%s\n" %
69 (empty, short(mmap2[f]), f, f))
57 (empty, short(mmap2[f]), f, f))
70 for f in removed:
58 for f in removed:
71 ui.write(":100664 000000 %s %s D\t%s\t%s\n" %
59 ui.write(":100664 000000 %s %s D\t%s\t%s\n" %
72 (short(mmap[f]), empty, f, f))
60 (short(mmap[f]), empty, f, f))
73 ##
61 ##
74
62
75 while True:
63 while True:
76 if opts['stdin']:
64 if opts['stdin']:
77 try:
65 try:
78 line = raw_input().split(' ')
66 line = raw_input().split(' ')
79 node1 = line[0]
67 node1 = line[0]
80 if len(line) > 1:
68 if len(line) > 1:
81 node2 = line[1]
69 node2 = line[1]
82 else:
70 else:
83 node2 = None
71 node2 = None
84 except EOFError:
72 except EOFError:
85 break
73 break
86 node1 = repo.lookup(node1)
74 node1 = repo.lookup(node1)
87 if node2:
75 if node2:
88 node2 = repo.lookup(node2)
76 node2 = repo.lookup(node2)
89 else:
77 else:
90 node2 = node1
78 node2 = node1
91 node1 = repo.changelog.parents(node1)[0]
79 node1 = repo.changelog.parents(node1)[0]
92 if opts['patch']:
80 if opts['patch']:
93 if opts['pretty']:
81 if opts['pretty']:
94 catcommit(ui, repo, node2, "")
82 catcommit(ui, repo, node2, "")
95 m = cmdutil.match(repo, files)
83 m = cmdutil.match(repo, files)
96 chunks = patch.diff(repo, node1, node2, match=m,
84 chunks = patch.diff(repo, node1, node2, match=m,
97 opts=patch.diffopts(ui, {'git': True}))
85 opts=patch.diffopts(ui, {'git': True}))
98 for chunk in chunks:
86 for chunk in chunks:
99 ui.write(chunk)
87 ui.write(chunk)
100 else:
88 else:
101 __difftree(repo, node1, node2, files=files)
89 __difftree(repo, node1, node2, files=files)
102 if not opts['stdin']:
90 if not opts['stdin']:
103 break
91 break
104
92
105 def catcommit(ui, repo, n, prefix, ctx=None):
93 def catcommit(ui, repo, n, prefix, ctx=None):
106 nlprefix = '\n' + prefix;
94 nlprefix = '\n' + prefix;
107 if ctx is None:
95 if ctx is None:
108 ctx = repo[n]
96 ctx = repo[n]
109 ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ??
97 ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ??
110 for p in ctx.parents():
98 for p in ctx.parents():
111 ui.write("parent %s\n" % p)
99 ui.write("parent %s\n" % p)
112
100
113 date = ctx.date()
101 date = ctx.date()
114 description = ctx.description().replace("\0", "")
102 description = ctx.description().replace("\0", "")
115 lines = description.splitlines()
103 lines = description.splitlines()
116 if lines and lines[-1].startswith('committer:'):
104 if lines and lines[-1].startswith('committer:'):
117 committer = lines[-1].split(': ')[1].rstrip()
105 committer = lines[-1].split(': ')[1].rstrip()
118 else:
106 else:
119 committer = ctx.user()
107 committer = ctx.user()
120
108
121 ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))
109 ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))
122 ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1]))
110 ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1]))
123 ui.write("revision %d\n" % ctx.rev())
111 ui.write("revision %d\n" % ctx.rev())
124 ui.write("branch %s\n\n" % ctx.branch())
112 ui.write("branch %s\n\n" % ctx.branch())
125
113
126 if prefix != "":
114 if prefix != "":
127 ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip()))
115 ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip()))
128 else:
116 else:
129 ui.write(description + "\n")
117 ui.write(description + "\n")
130 if prefix:
118 if prefix:
131 ui.write('\0')
119 ui.write('\0')
132
120
133 def base(ui, repo, node1, node2):
121 def base(ui, repo, node1, node2):
134 """output common ancestor information"""
122 """output common ancestor information"""
135 node1 = repo.lookup(node1)
123 node1 = repo.lookup(node1)
136 node2 = repo.lookup(node2)
124 node2 = repo.lookup(node2)
137 n = repo.changelog.ancestor(node1, node2)
125 n = repo.changelog.ancestor(node1, node2)
138 ui.write(short(n) + "\n")
126 ui.write(short(n) + "\n")
139
127
140 def catfile(ui, repo, type=None, r=None, **opts):
128 def catfile(ui, repo, type=None, r=None, **opts):
141 """cat a specific revision"""
129 """cat a specific revision"""
142 # in stdin mode, every line except the commit is prefixed with two
130 # in stdin mode, every line except the commit is prefixed with two
143 # spaces. This way the our caller can find the commit without magic
131 # spaces. This way the our caller can find the commit without magic
144 # strings
132 # strings
145 #
133 #
146 prefix = ""
134 prefix = ""
147 if opts['stdin']:
135 if opts['stdin']:
148 try:
136 try:
149 (type, r) = raw_input().split(' ');
137 (type, r) = raw_input().split(' ');
150 prefix = " "
138 prefix = " "
151 except EOFError:
139 except EOFError:
152 return
140 return
153
141
154 else:
142 else:
155 if not type or not r:
143 if not type or not r:
156 ui.warn(_("cat-file: type or revision not supplied\n"))
144 ui.warn(_("cat-file: type or revision not supplied\n"))
157 commands.help_(ui, 'cat-file')
145 commands.help_(ui, 'cat-file')
158
146
159 while r:
147 while r:
160 if type != "commit":
148 if type != "commit":
161 ui.warn(_("aborting hg cat-file only understands commits\n"))
149 ui.warn(_("aborting hg cat-file only understands commits\n"))
162 return 1;
150 return 1;
163 n = repo.lookup(r)
151 n = repo.lookup(r)
164 catcommit(ui, repo, n, prefix)
152 catcommit(ui, repo, n, prefix)
165 if opts['stdin']:
153 if opts['stdin']:
166 try:
154 try:
167 (type, r) = raw_input().split(' ');
155 (type, r) = raw_input().split(' ');
168 except EOFError:
156 except EOFError:
169 break
157 break
170 else:
158 else:
171 break
159 break
172
160
173 # git rev-tree is a confusing thing. You can supply a number of
161 # git rev-tree is a confusing thing. You can supply a number of
174 # commit sha1s on the command line, and it walks the commit history
162 # commit sha1s on the command line, and it walks the commit history
175 # telling you which commits are reachable from the supplied ones via
163 # telling you which commits are reachable from the supplied ones via
176 # a bitmask based on arg position.
164 # a bitmask based on arg position.
177 # you can specify a commit to stop at by starting the sha1 with ^
165 # you can specify a commit to stop at by starting the sha1 with ^
178 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
166 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
179 def chlogwalk():
167 def chlogwalk():
180 count = len(repo)
168 count = len(repo)
181 i = count
169 i = count
182 l = [0] * 100
170 l = [0] * 100
183 chunk = 100
171 chunk = 100
184 while True:
172 while True:
185 if chunk > i:
173 if chunk > i:
186 chunk = i
174 chunk = i
187 i = 0
175 i = 0
188 else:
176 else:
189 i -= chunk
177 i -= chunk
190
178
191 for x in xrange(chunk):
179 for x in xrange(chunk):
192 if i + x >= count:
180 if i + x >= count:
193 l[chunk - x:] = [0] * (chunk - x)
181 l[chunk - x:] = [0] * (chunk - x)
194 break
182 break
195 if full != None:
183 if full != None:
196 l[x] = repo[i + x]
184 l[x] = repo[i + x]
197 l[x].changeset() # force reading
185 l[x].changeset() # force reading
198 else:
186 else:
199 l[x] = 1
187 l[x] = 1
200 for x in xrange(chunk-1, -1, -1):
188 for x in xrange(chunk-1, -1, -1):
201 if l[x] != 0:
189 if l[x] != 0:
202 yield (i + x, full != None and l[x] or None)
190 yield (i + x, full != None and l[x] or None)
203 if i == 0:
191 if i == 0:
204 break
192 break
205
193
206 # calculate and return the reachability bitmask for sha
194 # calculate and return the reachability bitmask for sha
207 def is_reachable(ar, reachable, sha):
195 def is_reachable(ar, reachable, sha):
208 if len(ar) == 0:
196 if len(ar) == 0:
209 return 1
197 return 1
210 mask = 0
198 mask = 0
211 for i in xrange(len(ar)):
199 for i in xrange(len(ar)):
212 if sha in reachable[i]:
200 if sha in reachable[i]:
213 mask |= 1 << i
201 mask |= 1 << i
214
202
215 return mask
203 return mask
216
204
217 reachable = []
205 reachable = []
218 stop_sha1 = []
206 stop_sha1 = []
219 want_sha1 = []
207 want_sha1 = []
220 count = 0
208 count = 0
221
209
222 # figure out which commits they are asking for and which ones they
210 # figure out which commits they are asking for and which ones they
223 # want us to stop on
211 # want us to stop on
224 for i, arg in enumerate(args):
212 for i, arg in enumerate(args):
225 if arg.startswith('^'):
213 if arg.startswith('^'):
226 s = repo.lookup(arg[1:])
214 s = repo.lookup(arg[1:])
227 stop_sha1.append(s)
215 stop_sha1.append(s)
228 want_sha1.append(s)
216 want_sha1.append(s)
229 elif arg != 'HEAD':
217 elif arg != 'HEAD':
230 want_sha1.append(repo.lookup(arg))
218 want_sha1.append(repo.lookup(arg))
231
219
232 # calculate the graph for the supplied commits
220 # calculate the graph for the supplied commits
233 for i, n in enumerate(want_sha1):
221 for i, n in enumerate(want_sha1):
234 reachable.append(set());
222 reachable.append(set());
235 visit = [n];
223 visit = [n];
236 reachable[i].add(n)
224 reachable[i].add(n)
237 while visit:
225 while visit:
238 n = visit.pop(0)
226 n = visit.pop(0)
239 if n in stop_sha1:
227 if n in stop_sha1:
240 continue
228 continue
241 for p in repo.changelog.parents(n):
229 for p in repo.changelog.parents(n):
242 if p not in reachable[i]:
230 if p not in reachable[i]:
243 reachable[i].add(p)
231 reachable[i].add(p)
244 visit.append(p)
232 visit.append(p)
245 if p in stop_sha1:
233 if p in stop_sha1:
246 continue
234 continue
247
235
248 # walk the repository looking for commits that are in our
236 # walk the repository looking for commits that are in our
249 # reachability graph
237 # reachability graph
250 for i, ctx in chlogwalk():
238 for i, ctx in chlogwalk():
251 n = repo.changelog.node(i)
239 n = repo.changelog.node(i)
252 mask = is_reachable(want_sha1, reachable, n)
240 mask = is_reachable(want_sha1, reachable, n)
253 if mask:
241 if mask:
254 parentstr = ""
242 parentstr = ""
255 if parents:
243 if parents:
256 pp = repo.changelog.parents(n)
244 pp = repo.changelog.parents(n)
257 if pp[0] != nullid:
245 if pp[0] != nullid:
258 parentstr += " " + short(pp[0])
246 parentstr += " " + short(pp[0])
259 if pp[1] != nullid:
247 if pp[1] != nullid:
260 parentstr += " " + short(pp[1])
248 parentstr += " " + short(pp[1])
261 if not full:
249 if not full:
262 ui.write("%s%s\n" % (short(n), parentstr))
250 ui.write("%s%s\n" % (short(n), parentstr))
263 elif full == "commit":
251 elif full == "commit":
264 ui.write("%s%s\n" % (short(n), parentstr))
252 ui.write("%s%s\n" % (short(n), parentstr))
265 catcommit(ui, repo, n, ' ', ctx)
253 catcommit(ui, repo, n, ' ', ctx)
266 else:
254 else:
267 (p1, p2) = repo.changelog.parents(n)
255 (p1, p2) = repo.changelog.parents(n)
268 (h, h1, h2) = map(short, (n, p1, p2))
256 (h, h1, h2) = map(short, (n, p1, p2))
269 (i1, i2) = map(repo.changelog.rev, (p1, p2))
257 (i1, i2) = map(repo.changelog.rev, (p1, p2))
270
258
271 date = ctx.date()[0]
259 date = ctx.date()[0]
272 ui.write("%s %s:%s" % (date, h, mask))
260 ui.write("%s %s:%s" % (date, h, mask))
273 mask = is_reachable(want_sha1, reachable, p1)
261 mask = is_reachable(want_sha1, reachable, p1)
274 if i1 != nullrev and mask > 0:
262 if i1 != nullrev and mask > 0:
275 ui.write("%s:%s " % (h1, mask)),
263 ui.write("%s:%s " % (h1, mask)),
276 mask = is_reachable(want_sha1, reachable, p2)
264 mask = is_reachable(want_sha1, reachable, p2)
277 if i2 != nullrev and mask > 0:
265 if i2 != nullrev and mask > 0:
278 ui.write("%s:%s " % (h2, mask))
266 ui.write("%s:%s " % (h2, mask))
279 ui.write("\n")
267 ui.write("\n")
280 if maxnr and count >= maxnr:
268 if maxnr and count >= maxnr:
281 break
269 break
282 count += 1
270 count += 1
283
271
284 def revparse(ui, repo, *revs, **opts):
272 def revparse(ui, repo, *revs, **opts):
285 """parse given revisions"""
273 """parse given revisions"""
286 def revstr(rev):
274 def revstr(rev):
287 if rev == 'HEAD':
275 if rev == 'HEAD':
288 rev = 'tip'
276 rev = 'tip'
289 return revlog.hex(repo.lookup(rev))
277 return revlog.hex(repo.lookup(rev))
290
278
291 for r in revs:
279 for r in revs:
292 revrange = r.split(':', 1)
280 revrange = r.split(':', 1)
293 ui.write('%s\n' % revstr(revrange[0]))
281 ui.write('%s\n' % revstr(revrange[0]))
294 if len(revrange) == 2:
282 if len(revrange) == 2:
295 ui.write('^%s\n' % revstr(revrange[1]))
283 ui.write('^%s\n' % revstr(revrange[1]))
296
284
297 # git rev-list tries to order things by date, and has the ability to stop
285 # git rev-list tries to order things by date, and has the ability to stop
298 # at a given commit without walking the whole repo. TODO add the stop
286 # at a given commit without walking the whole repo. TODO add the stop
299 # parameter
287 # parameter
300 def revlist(ui, repo, *revs, **opts):
288 def revlist(ui, repo, *revs, **opts):
301 """print revisions"""
289 """print revisions"""
302 if opts['header']:
290 if opts['header']:
303 full = "commit"
291 full = "commit"
304 else:
292 else:
305 full = None
293 full = None
306 copy = [x for x in revs]
294 copy = [x for x in revs]
307 revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
295 revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
308
296
309 def config(ui, repo, **opts):
297 def config(ui, repo, **opts):
310 """print extension options"""
298 """print extension options"""
311 def writeopt(name, value):
299 def writeopt(name, value):
312 ui.write('k=%s\nv=%s\n' % (name, value))
300 ui.write('k=%s\nv=%s\n' % (name, value))
313
301
314 writeopt('vdiff', ui.config('hgk', 'vdiff', ''))
302 writeopt('vdiff', ui.config('hgk', 'vdiff', ''))
315
303
316
304
317 def view(ui, repo, *etc, **opts):
305 def view(ui, repo, *etc, **opts):
318 "start interactive history viewer"
306 "start interactive history viewer"
319 os.chdir(repo.root)
307 os.chdir(repo.root)
320 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
308 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
321 cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc))
309 cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc))
322 ui.debug(_("running %s\n") % cmd)
310 ui.debug(_("running %s\n") % cmd)
323 util.system(cmd)
311 util.system(cmd)
324
312
325 cmdtable = {
313 cmdtable = {
326 "^view":
314 "^view":
327 (view,
315 (view,
328 [('l', 'limit', '', _('limit number of changes displayed'))],
316 [('l', 'limit', '', _('limit number of changes displayed'))],
329 _('hg view [-l LIMIT] [REVRANGE]')),
317 _('hg view [-l LIMIT] [REVRANGE]')),
330 "debug-diff-tree":
318 "debug-diff-tree":
331 (difftree,
319 (difftree,
332 [('p', 'patch', None, _('generate patch')),
320 [('p', 'patch', None, _('generate patch')),
333 ('r', 'recursive', None, _('recursive')),
321 ('r', 'recursive', None, _('recursive')),
334 ('P', 'pretty', None, _('pretty')),
322 ('P', 'pretty', None, _('pretty')),
335 ('s', 'stdin', None, _('stdin')),
323 ('s', 'stdin', None, _('stdin')),
336 ('C', 'copy', None, _('detect copies')),
324 ('C', 'copy', None, _('detect copies')),
337 ('S', 'search', "", _('search'))],
325 ('S', 'search', "", _('search'))],
338 _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')),
326 _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')),
339 "debug-cat-file":
327 "debug-cat-file":
340 (catfile,
328 (catfile,
341 [('s', 'stdin', None, _('stdin'))],
329 [('s', 'stdin', None, _('stdin'))],
342 _('hg debug-cat-file [OPTION]... TYPE FILE')),
330 _('hg debug-cat-file [OPTION]... TYPE FILE')),
343 "debug-config":
331 "debug-config":
344 (config, [], _('hg debug-config')),
332 (config, [], _('hg debug-config')),
345 "debug-merge-base":
333 "debug-merge-base":
346 (base, [], _('hg debug-merge-base node node')),
334 (base, [], _('hg debug-merge-base node node')),
347 "debug-rev-parse":
335 "debug-rev-parse":
348 (revparse,
336 (revparse,
349 [('', 'default', '', _('ignored'))],
337 [('', 'default', '', _('ignored'))],
350 _('hg debug-rev-parse REV')),
338 _('hg debug-rev-parse REV')),
351 "debug-rev-list":
339 "debug-rev-list":
352 (revlist,
340 (revlist,
353 [('H', 'header', None, _('header')),
341 [('H', 'header', None, _('header')),
354 ('t', 'topo-order', None, _('topo-order')),
342 ('t', 'topo-order', None, _('topo-order')),
355 ('p', 'parents', None, _('parents')),
343 ('p', 'parents', None, _('parents')),
356 ('n', 'max-count', 0, _('max-count'))],
344 ('n', 'max-count', 0, _('max-count'))],
357 _('hg debug-rev-list [options] revs')),
345 _('hg debug-rev-list [options] revs')),
358 }
346 }
@@ -1,67 +1,62 b''
1 # highlight - syntax highlighting in hgweb, based on Pygments
1 # highlight - syntax highlighting in hgweb, based on Pygments
2 #
2 #
3 # Copyright 2008, 2009 Patrick Mezard <pmezard@gmail.com> and others
3 # Copyright 2008, 2009 Patrick Mezard <pmezard@gmail.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7 #
7 #
8 # The original module was split in an interface and an implementation
8 # The original module was split in an interface and an implementation
9 # file to defer pygments loading and speedup extension setup.
9 # file to defer pygments loading and speedup extension setup.
10
10
11 """syntax highlighting in hgweb, based on Pygments
11 """syntax highlighting in hgweb, based on Pygments
12
12
13 It depends on the Pygments syntax highlighting library:
13 It depends on the Pygments syntax highlighting library:
14 http://pygments.org/
14 http://pygments.org/
15
15
16 To enable the extension add this to hgrc:
17
18 [extensions]
19 hgext.highlight =
20
21 There is a single configuration option:
16 There is a single configuration option:
22
17
23 [web]
18 [web]
24 pygments_style = <style>
19 pygments_style = <style>
25
20
26 The default is 'colorful'.
21 The default is 'colorful'.
27
22
28 -- Adam Hupp <adam@hupp.org>
23 -- Adam Hupp <adam@hupp.org>
29 """
24 """
30
25
31 import highlight
26 import highlight
32 from mercurial.hgweb import webcommands, webutil, common
27 from mercurial.hgweb import webcommands, webutil, common
33 from mercurial import extensions
28 from mercurial import extensions
34
29
35 def filerevision_highlight(orig, web, tmpl, fctx):
30 def filerevision_highlight(orig, web, tmpl, fctx):
36 mt = ''.join(tmpl('mimetype', encoding=web.encoding))
31 mt = ''.join(tmpl('mimetype', encoding=web.encoding))
37 # only pygmentize for mimetype containing 'html' so we both match
32 # only pygmentize for mimetype containing 'html' so we both match
38 # 'text/html' and possibly 'application/xhtml+xml' in the future
33 # 'text/html' and possibly 'application/xhtml+xml' in the future
39 # so that we don't have to touch the extension when the mimetype
34 # so that we don't have to touch the extension when the mimetype
40 # for a template changes; also hgweb optimizes the case that a
35 # for a template changes; also hgweb optimizes the case that a
41 # raw file is sent using rawfile() and doesn't call us, so we
36 # raw file is sent using rawfile() and doesn't call us, so we
42 # can't clash with the file's content-type here in case we
37 # can't clash with the file's content-type here in case we
43 # pygmentize a html file
38 # pygmentize a html file
44 if 'html' in mt:
39 if 'html' in mt:
45 style = web.config('web', 'pygments_style', 'colorful')
40 style = web.config('web', 'pygments_style', 'colorful')
46 highlight.pygmentize('fileline', fctx, style, tmpl)
41 highlight.pygmentize('fileline', fctx, style, tmpl)
47 return orig(web, tmpl, fctx)
42 return orig(web, tmpl, fctx)
48
43
49 def annotate_highlight(orig, web, req, tmpl):
44 def annotate_highlight(orig, web, req, tmpl):
50 mt = ''.join(tmpl('mimetype', encoding=web.encoding))
45 mt = ''.join(tmpl('mimetype', encoding=web.encoding))
51 if 'html' in mt:
46 if 'html' in mt:
52 fctx = webutil.filectx(web.repo, req)
47 fctx = webutil.filectx(web.repo, req)
53 style = web.config('web', 'pygments_style', 'colorful')
48 style = web.config('web', 'pygments_style', 'colorful')
54 highlight.pygmentize('annotateline', fctx, style, tmpl)
49 highlight.pygmentize('annotateline', fctx, style, tmpl)
55 return orig(web, req, tmpl)
50 return orig(web, req, tmpl)
56
51
57 def generate_css(web, req, tmpl):
52 def generate_css(web, req, tmpl):
58 pg_style = web.config('web', 'pygments_style', 'colorful')
53 pg_style = web.config('web', 'pygments_style', 'colorful')
59 fmter = highlight.HtmlFormatter(style = pg_style)
54 fmter = highlight.HtmlFormatter(style = pg_style)
60 req.respond(common.HTTP_OK, 'text/css')
55 req.respond(common.HTTP_OK, 'text/css')
61 return ['/* pygments_style = %s */\n\n' % pg_style, fmter.get_style_defs('')]
56 return ['/* pygments_style = %s */\n\n' % pg_style, fmter.get_style_defs('')]
62
57
63 # monkeypatch in the new version
58 # monkeypatch in the new version
64 extensions.wrapfunction(webcommands, '_filerevision', filerevision_highlight)
59 extensions.wrapfunction(webcommands, '_filerevision', filerevision_highlight)
65 extensions.wrapfunction(webcommands, 'annotate', annotate_highlight)
60 extensions.wrapfunction(webcommands, 'annotate', annotate_highlight)
66 webcommands.highlightcss = generate_css
61 webcommands.highlightcss = generate_css
67 webcommands.__all__.append('highlightcss')
62 webcommands.__all__.append('highlightcss')
@@ -1,84 +1,80 b''
1 # interhg.py - interhg
1 # interhg.py - interhg
2 #
2 #
3 # Copyright 2007 OHASHI Hideya <ohachige@gmail.com>
3 # Copyright 2007 OHASHI Hideya <ohachige@gmail.com>
4 #
4 #
5 # Contributor(s):
5 # Contributor(s):
6 # Edward Lee <edward.lee@engineering.uiuc.edu>
6 # Edward Lee <edward.lee@engineering.uiuc.edu>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2, incorporated herein by reference.
9 # GNU General Public License version 2, incorporated herein by reference.
10
10
11 '''expand expressions into changelog and summaries
11 '''expand expressions into changelog and summaries
12
12
13 This extension allows the use of a special syntax in summaries,
13 This extension allows the use of a special syntax in summaries,
14 which will be automatically expanded into links or any other
14 which will be automatically expanded into links or any other
15 arbitrary expression, much like InterWiki does.
15 arbitrary expression, much like InterWiki does.
16
16
17 To enable this extension, add the following lines to your hgrc:
17 A few example patterns (link to bug tracking, etc.) that may
18
18 be used in your hgrc:
19 [extensions]
20 interhg =
21
22 A few example patterns (link to bug tracking, etc.):
23
19
24 [interhg]
20 [interhg]
25 issues = s!issue(\d+)!<a href="http://bts/issue\1">issue\1<\/a>!
21 issues = s!issue(\d+)!<a href="http://bts/issue\1">issue\1<\/a>!
26 bugzilla = s!((?:bug|b=|(?=#?\d{4,}))(?:\s*#?)(\d+))!<a..=\2">\1</a>!i
22 bugzilla = s!((?:bug|b=|(?=#?\d{4,}))(?:\s*#?)(\d+))!<a..=\2">\1</a>!i
27 boldify = s/(^|\s)#(\d+)\b/ <b>#\2<\/b>/
23 boldify = s/(^|\s)#(\d+)\b/ <b>#\2<\/b>/
28 '''
24 '''
29
25
30 import re
26 import re
31 from mercurial.hgweb import hgweb_mod
27 from mercurial.hgweb import hgweb_mod
32 from mercurial import templatefilters, extensions
28 from mercurial import templatefilters, extensions
33 from mercurial.i18n import _
29 from mercurial.i18n import _
34
30
35 orig_escape = templatefilters.filters["escape"]
31 orig_escape = templatefilters.filters["escape"]
36
32
37 interhg_table = []
33 interhg_table = []
38
34
39 def interhg_escape(x):
35 def interhg_escape(x):
40 escstr = orig_escape(x)
36 escstr = orig_escape(x)
41 for regexp, format in interhg_table:
37 for regexp, format in interhg_table:
42 escstr = regexp.sub(format, escstr)
38 escstr = regexp.sub(format, escstr)
43 return escstr
39 return escstr
44
40
45 templatefilters.filters["escape"] = interhg_escape
41 templatefilters.filters["escape"] = interhg_escape
46
42
47 def interhg_refresh(orig, self):
43 def interhg_refresh(orig, self):
48 interhg_table[:] = []
44 interhg_table[:] = []
49 for key, pattern in self.repo.ui.configitems('interhg'):
45 for key, pattern in self.repo.ui.configitems('interhg'):
50 # grab the delimiter from the character after the "s"
46 # grab the delimiter from the character after the "s"
51 unesc = pattern[1]
47 unesc = pattern[1]
52 delim = re.escape(unesc)
48 delim = re.escape(unesc)
53
49
54 # identify portions of the pattern, taking care to avoid escaped
50 # identify portions of the pattern, taking care to avoid escaped
55 # delimiters. the replace format and flags are optional, but delimiters
51 # delimiters. the replace format and flags are optional, but delimiters
56 # are required.
52 # are required.
57 match = re.match(r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
53 match = re.match(r'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
58 % (delim, delim, delim), pattern)
54 % (delim, delim, delim), pattern)
59 if not match:
55 if not match:
60 self.repo.ui.warn(_("interhg: invalid pattern for %s: %s\n")
56 self.repo.ui.warn(_("interhg: invalid pattern for %s: %s\n")
61 % (key, pattern))
57 % (key, pattern))
62 continue
58 continue
63
59
64 # we need to unescape the delimiter for regexp and format
60 # we need to unescape the delimiter for regexp and format
65 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
61 delim_re = re.compile(r'(?<!\\)\\%s' % delim)
66 regexp = delim_re.sub(unesc, match.group(1))
62 regexp = delim_re.sub(unesc, match.group(1))
67 format = delim_re.sub(unesc, match.group(2))
63 format = delim_re.sub(unesc, match.group(2))
68
64
69 # the pattern allows for 6 regexp flags, so set them if necessary
65 # the pattern allows for 6 regexp flags, so set them if necessary
70 flagin = match.group(3)
66 flagin = match.group(3)
71 flags = 0
67 flags = 0
72 if flagin:
68 if flagin:
73 for flag in flagin.upper():
69 for flag in flagin.upper():
74 flags |= re.__dict__[flag]
70 flags |= re.__dict__[flag]
75
71
76 try:
72 try:
77 regexp = re.compile(regexp, flags)
73 regexp = re.compile(regexp, flags)
78 interhg_table.append((regexp, format))
74 interhg_table.append((regexp, format))
79 except re.error:
75 except re.error:
80 self.repo.ui.warn(_("interhg: invalid regexp for %s: %s\n")
76 self.repo.ui.warn(_("interhg: invalid regexp for %s: %s\n")
81 % (key, regexp))
77 % (key, regexp))
82 return orig(self)
78 return orig(self)
83
79
84 extensions.wrapfunction(hgweb_mod.hgweb, 'refresh', interhg_refresh)
80 extensions.wrapfunction(hgweb_mod.hgweb, 'refresh', interhg_refresh)
@@ -1,534 +1,528 b''
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7 #
7 #
8 # $Id$
8 # $Id$
9 #
9 #
10 # Keyword expansion hack against the grain of a DSCM
10 # Keyword expansion hack against the grain of a DSCM
11 #
11 #
12 # There are many good reasons why this is not needed in a distributed
12 # There are many good reasons why this is not needed in a distributed
13 # SCM, still it may be useful in very small projects based on single
13 # SCM, still it may be useful in very small projects based on single
14 # files (like LaTeX packages), that are mostly addressed to an
14 # files (like LaTeX packages), that are mostly addressed to an
15 # audience not running a version control system.
15 # audience not running a version control system.
16 #
16 #
17 # For in-depth discussion refer to
17 # For in-depth discussion refer to
18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
19 #
19 #
20 # Keyword expansion is based on Mercurial's changeset template mappings.
20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 #
21 #
22 # Binary files are not touched.
22 # Binary files are not touched.
23 #
23 #
24 # Setup in hgrc:
25 #
26 # [extensions]
27 # # enable extension
28 # hgext.keyword =
29 #
30 # Files to act upon/ignore are specified in the [keyword] section.
24 # Files to act upon/ignore are specified in the [keyword] section.
31 # Customized keyword template mappings in the [keywordmaps] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
32 #
26 #
33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34
28
35 '''keyword expansion in tracked files
29 '''keyword expansion in tracked files
36
30
37 This extension expands RCS/CVS-like or self-customized $Keywords$ in
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
38 tracked text files selected by your configuration.
32 tracked text files selected by your configuration.
39
33
40 Keywords are only expanded in local repositories and not stored in the
34 Keywords are only expanded in local repositories and not stored in the
41 change history. The mechanism can be regarded as a convenience for the
35 change history. The mechanism can be regarded as a convenience for the
42 current user or for archive distribution.
36 current user or for archive distribution.
43
37
44 Configuration is done in the [keyword] and [keywordmaps] sections of
38 Configuration is done in the [keyword] and [keywordmaps] sections of
45 hgrc files.
39 hgrc files.
46
40
47 Example:
41 Example:
48
42
49 [keyword]
43 [keyword]
50 # expand keywords in every python file except those matching "x*"
44 # expand keywords in every python file except those matching "x*"
51 **.py =
45 **.py =
52 x* = ignore
46 x* = ignore
53
47
54 Note: the more specific you are in your filename patterns
48 Note: the more specific you are in your filename patterns
55 the less you lose speed in huge repositories.
49 the less you lose speed in huge repositories.
56
50
57 For [keywordmaps] template mapping and expansion demonstration and
51 For [keywordmaps] template mapping and expansion demonstration and
58 control run "hg kwdemo".
52 control run "hg kwdemo".
59
53
60 An additional date template filter {date|utcdate} is provided.
54 An additional date template filter {date|utcdate} is provided.
61
55
62 The default template mappings (view with "hg kwdemo -d") can be
56 The default template mappings (view with "hg kwdemo -d") can be
63 replaced with customized keywords and templates. Again, run "hg
57 replaced with customized keywords and templates. Again, run "hg
64 kwdemo" to control the results of your config changes.
58 kwdemo" to control the results of your config changes.
65
59
66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
60 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 the risk of inadvertently storing expanded keywords in the change
61 the risk of inadvertently storing expanded keywords in the change
68 history.
62 history.
69
63
70 To force expansion after enabling it, or a configuration change, run
64 To force expansion after enabling it, or a configuration change, run
71 "hg kwexpand".
65 "hg kwexpand".
72
66
73 Also, when committing with the record extension or using mq's qrecord,
67 Also, when committing with the record extension or using mq's qrecord,
74 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
68 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
75 the files in question to update keyword expansions after all changes
69 the files in question to update keyword expansions after all changes
76 have been checked in.
70 have been checked in.
77
71
78 Expansions spanning more than one line and incremental expansions,
72 Expansions spanning more than one line and incremental expansions,
79 like CVS' $Log$, are not supported. A keyword template map
73 like CVS' $Log$, are not supported. A keyword template map
80 "Log = {desc}" expands to the first line of the changeset description.
74 "Log = {desc}" expands to the first line of the changeset description.
81 '''
75 '''
82
76
83 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
77 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
84 from mercurial import patch, localrepo, templater, templatefilters, util, match
78 from mercurial import patch, localrepo, templater, templatefilters, util, match
85 from mercurial.hgweb import webcommands
79 from mercurial.hgweb import webcommands
86 from mercurial.lock import release
80 from mercurial.lock import release
87 from mercurial.node import nullid, hex
81 from mercurial.node import nullid, hex
88 from mercurial.i18n import _
82 from mercurial.i18n import _
89 import re, shutil, tempfile, time
83 import re, shutil, tempfile, time
90
84
91 commands.optionalrepo += ' kwdemo'
85 commands.optionalrepo += ' kwdemo'
92
86
93 # hg commands that do not act on keywords
87 # hg commands that do not act on keywords
94 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
88 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
95 ' log outgoing push rename rollback tip verify'
89 ' log outgoing push rename rollback tip verify'
96 ' convert email glog')
90 ' convert email glog')
97
91
98 # hg commands that trigger expansion only when writing to working dir,
92 # hg commands that trigger expansion only when writing to working dir,
99 # not when reading filelog, and unexpand when reading from working dir
93 # not when reading filelog, and unexpand when reading from working dir
100 restricted = 'merge record resolve qfold qimport qnew qpush qrefresh qrecord'
94 restricted = 'merge record resolve qfold qimport qnew qpush qrefresh qrecord'
101
95
102 def utcdate(date):
96 def utcdate(date):
103 '''Returns hgdate in cvs-like UTC format.'''
97 '''Returns hgdate in cvs-like UTC format.'''
104 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
98 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
105
99
106 # make keyword tools accessible
100 # make keyword tools accessible
107 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
101 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
108
102
109
103
110 class kwtemplater(object):
104 class kwtemplater(object):
111 '''
105 '''
112 Sets up keyword templates, corresponding keyword regex, and
106 Sets up keyword templates, corresponding keyword regex, and
113 provides keyword substitution functions.
107 provides keyword substitution functions.
114 '''
108 '''
115 templates = {
109 templates = {
116 'Revision': '{node|short}',
110 'Revision': '{node|short}',
117 'Author': '{author|user}',
111 'Author': '{author|user}',
118 'Date': '{date|utcdate}',
112 'Date': '{date|utcdate}',
119 'RCSFile': '{file|basename},v',
113 'RCSFile': '{file|basename},v',
120 'Source': '{root}/{file},v',
114 'Source': '{root}/{file},v',
121 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
115 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
122 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
116 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
123 }
117 }
124
118
125 def __init__(self, ui, repo):
119 def __init__(self, ui, repo):
126 self.ui = ui
120 self.ui = ui
127 self.repo = repo
121 self.repo = repo
128 self.match = match.match(repo.root, '', [],
122 self.match = match.match(repo.root, '', [],
129 kwtools['inc'], kwtools['exc'])
123 kwtools['inc'], kwtools['exc'])
130 self.restrict = kwtools['hgcmd'] in restricted.split()
124 self.restrict = kwtools['hgcmd'] in restricted.split()
131
125
132 kwmaps = self.ui.configitems('keywordmaps')
126 kwmaps = self.ui.configitems('keywordmaps')
133 if kwmaps: # override default templates
127 if kwmaps: # override default templates
134 kwmaps = [(k, templater.parsestring(v, False))
128 kwmaps = [(k, templater.parsestring(v, False))
135 for (k, v) in kwmaps]
129 for (k, v) in kwmaps]
136 self.templates = dict(kwmaps)
130 self.templates = dict(kwmaps)
137 escaped = map(re.escape, self.templates.keys())
131 escaped = map(re.escape, self.templates.keys())
138 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
132 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
139 self.re_kw = re.compile(kwpat)
133 self.re_kw = re.compile(kwpat)
140
134
141 templatefilters.filters['utcdate'] = utcdate
135 templatefilters.filters['utcdate'] = utcdate
142 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
136 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
143 False, None, '', False)
137 False, None, '', False)
144
138
145 def substitute(self, data, path, ctx, subfunc):
139 def substitute(self, data, path, ctx, subfunc):
146 '''Replaces keywords in data with expanded template.'''
140 '''Replaces keywords in data with expanded template.'''
147 def kwsub(mobj):
141 def kwsub(mobj):
148 kw = mobj.group(1)
142 kw = mobj.group(1)
149 self.ct.use_template(self.templates[kw])
143 self.ct.use_template(self.templates[kw])
150 self.ui.pushbuffer()
144 self.ui.pushbuffer()
151 self.ct.show(ctx, root=self.repo.root, file=path)
145 self.ct.show(ctx, root=self.repo.root, file=path)
152 ekw = templatefilters.firstline(self.ui.popbuffer())
146 ekw = templatefilters.firstline(self.ui.popbuffer())
153 return '$%s: %s $' % (kw, ekw)
147 return '$%s: %s $' % (kw, ekw)
154 return subfunc(kwsub, data)
148 return subfunc(kwsub, data)
155
149
156 def expand(self, path, node, data):
150 def expand(self, path, node, data):
157 '''Returns data with keywords expanded.'''
151 '''Returns data with keywords expanded.'''
158 if not self.restrict and self.match(path) and not util.binary(data):
152 if not self.restrict and self.match(path) and not util.binary(data):
159 ctx = self.repo.filectx(path, fileid=node).changectx()
153 ctx = self.repo.filectx(path, fileid=node).changectx()
160 return self.substitute(data, path, ctx, self.re_kw.sub)
154 return self.substitute(data, path, ctx, self.re_kw.sub)
161 return data
155 return data
162
156
163 def iskwfile(self, path, flagfunc):
157 def iskwfile(self, path, flagfunc):
164 '''Returns true if path matches [keyword] pattern
158 '''Returns true if path matches [keyword] pattern
165 and is not a symbolic link.
159 and is not a symbolic link.
166 Caveat: localrepository._link fails on Windows.'''
160 Caveat: localrepository._link fails on Windows.'''
167 return self.match(path) and not 'l' in flagfunc(path)
161 return self.match(path) and not 'l' in flagfunc(path)
168
162
169 def overwrite(self, node, expand, files):
163 def overwrite(self, node, expand, files):
170 '''Overwrites selected files expanding/shrinking keywords.'''
164 '''Overwrites selected files expanding/shrinking keywords.'''
171 ctx = self.repo[node]
165 ctx = self.repo[node]
172 mf = ctx.manifest()
166 mf = ctx.manifest()
173 if node is not None: # commit
167 if node is not None: # commit
174 files = [f for f in ctx.files() if f in mf]
168 files = [f for f in ctx.files() if f in mf]
175 notify = self.ui.debug
169 notify = self.ui.debug
176 else: # kwexpand/kwshrink
170 else: # kwexpand/kwshrink
177 notify = self.ui.note
171 notify = self.ui.note
178 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
172 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
179 if candidates:
173 if candidates:
180 self.restrict = True # do not expand when reading
174 self.restrict = True # do not expand when reading
181 msg = (expand and _('overwriting %s expanding keywords\n')
175 msg = (expand and _('overwriting %s expanding keywords\n')
182 or _('overwriting %s shrinking keywords\n'))
176 or _('overwriting %s shrinking keywords\n'))
183 for f in candidates:
177 for f in candidates:
184 fp = self.repo.file(f)
178 fp = self.repo.file(f)
185 data = fp.read(mf[f])
179 data = fp.read(mf[f])
186 if util.binary(data):
180 if util.binary(data):
187 continue
181 continue
188 if expand:
182 if expand:
189 if node is None:
183 if node is None:
190 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
184 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
191 data, found = self.substitute(data, f, ctx,
185 data, found = self.substitute(data, f, ctx,
192 self.re_kw.subn)
186 self.re_kw.subn)
193 else:
187 else:
194 found = self.re_kw.search(data)
188 found = self.re_kw.search(data)
195 if found:
189 if found:
196 notify(msg % f)
190 notify(msg % f)
197 self.repo.wwrite(f, data, mf.flags(f))
191 self.repo.wwrite(f, data, mf.flags(f))
198 self.repo.dirstate.normal(f)
192 self.repo.dirstate.normal(f)
199 self.restrict = False
193 self.restrict = False
200
194
201 def shrinktext(self, text):
195 def shrinktext(self, text):
202 '''Unconditionally removes all keyword substitutions from text.'''
196 '''Unconditionally removes all keyword substitutions from text.'''
203 return self.re_kw.sub(r'$\1$', text)
197 return self.re_kw.sub(r'$\1$', text)
204
198
205 def shrink(self, fname, text):
199 def shrink(self, fname, text):
206 '''Returns text with all keyword substitutions removed.'''
200 '''Returns text with all keyword substitutions removed.'''
207 if self.match(fname) and not util.binary(text):
201 if self.match(fname) and not util.binary(text):
208 return self.shrinktext(text)
202 return self.shrinktext(text)
209 return text
203 return text
210
204
211 def shrinklines(self, fname, lines):
205 def shrinklines(self, fname, lines):
212 '''Returns lines with keyword substitutions removed.'''
206 '''Returns lines with keyword substitutions removed.'''
213 if self.match(fname):
207 if self.match(fname):
214 text = ''.join(lines)
208 text = ''.join(lines)
215 if not util.binary(text):
209 if not util.binary(text):
216 return self.shrinktext(text).splitlines(True)
210 return self.shrinktext(text).splitlines(True)
217 return lines
211 return lines
218
212
219 def wread(self, fname, data):
213 def wread(self, fname, data):
220 '''If in restricted mode returns data read from wdir with
214 '''If in restricted mode returns data read from wdir with
221 keyword substitutions removed.'''
215 keyword substitutions removed.'''
222 return self.restrict and self.shrink(fname, data) or data
216 return self.restrict and self.shrink(fname, data) or data
223
217
224 class kwfilelog(filelog.filelog):
218 class kwfilelog(filelog.filelog):
225 '''
219 '''
226 Subclass of filelog to hook into its read, add, cmp methods.
220 Subclass of filelog to hook into its read, add, cmp methods.
227 Keywords are "stored" unexpanded, and processed on reading.
221 Keywords are "stored" unexpanded, and processed on reading.
228 '''
222 '''
229 def __init__(self, opener, kwt, path):
223 def __init__(self, opener, kwt, path):
230 super(kwfilelog, self).__init__(opener, path)
224 super(kwfilelog, self).__init__(opener, path)
231 self.kwt = kwt
225 self.kwt = kwt
232 self.path = path
226 self.path = path
233
227
234 def read(self, node):
228 def read(self, node):
235 '''Expands keywords when reading filelog.'''
229 '''Expands keywords when reading filelog.'''
236 data = super(kwfilelog, self).read(node)
230 data = super(kwfilelog, self).read(node)
237 return self.kwt.expand(self.path, node, data)
231 return self.kwt.expand(self.path, node, data)
238
232
239 def add(self, text, meta, tr, link, p1=None, p2=None):
233 def add(self, text, meta, tr, link, p1=None, p2=None):
240 '''Removes keyword substitutions when adding to filelog.'''
234 '''Removes keyword substitutions when adding to filelog.'''
241 text = self.kwt.shrink(self.path, text)
235 text = self.kwt.shrink(self.path, text)
242 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
236 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
243
237
244 def cmp(self, node, text):
238 def cmp(self, node, text):
245 '''Removes keyword substitutions for comparison.'''
239 '''Removes keyword substitutions for comparison.'''
246 text = self.kwt.shrink(self.path, text)
240 text = self.kwt.shrink(self.path, text)
247 if self.renamed(node):
241 if self.renamed(node):
248 t2 = super(kwfilelog, self).read(node)
242 t2 = super(kwfilelog, self).read(node)
249 return t2 != text
243 return t2 != text
250 return revlog.revlog.cmp(self, node, text)
244 return revlog.revlog.cmp(self, node, text)
251
245
252 def _status(ui, repo, kwt, unknown, *pats, **opts):
246 def _status(ui, repo, kwt, unknown, *pats, **opts):
253 '''Bails out if [keyword] configuration is not active.
247 '''Bails out if [keyword] configuration is not active.
254 Returns status of working directory.'''
248 Returns status of working directory.'''
255 if kwt:
249 if kwt:
256 match = cmdutil.match(repo, pats, opts)
250 match = cmdutil.match(repo, pats, opts)
257 return repo.status(match=match, unknown=unknown, clean=True)
251 return repo.status(match=match, unknown=unknown, clean=True)
258 if ui.configitems('keyword'):
252 if ui.configitems('keyword'):
259 raise util.Abort(_('[keyword] patterns cannot match'))
253 raise util.Abort(_('[keyword] patterns cannot match'))
260 raise util.Abort(_('no [keyword] patterns configured'))
254 raise util.Abort(_('no [keyword] patterns configured'))
261
255
262 def _kwfwrite(ui, repo, expand, *pats, **opts):
256 def _kwfwrite(ui, repo, expand, *pats, **opts):
263 '''Selects files and passes them to kwtemplater.overwrite.'''
257 '''Selects files and passes them to kwtemplater.overwrite.'''
264 if repo.dirstate.parents()[1] != nullid:
258 if repo.dirstate.parents()[1] != nullid:
265 raise util.Abort(_('outstanding uncommitted merge'))
259 raise util.Abort(_('outstanding uncommitted merge'))
266 kwt = kwtools['templater']
260 kwt = kwtools['templater']
267 status = _status(ui, repo, kwt, False, *pats, **opts)
261 status = _status(ui, repo, kwt, False, *pats, **opts)
268 modified, added, removed, deleted = status[:4]
262 modified, added, removed, deleted = status[:4]
269 if modified or added or removed or deleted:
263 if modified or added or removed or deleted:
270 raise util.Abort(_('outstanding uncommitted changes'))
264 raise util.Abort(_('outstanding uncommitted changes'))
271 wlock = lock = None
265 wlock = lock = None
272 try:
266 try:
273 wlock = repo.wlock()
267 wlock = repo.wlock()
274 lock = repo.lock()
268 lock = repo.lock()
275 kwt.overwrite(None, expand, status[6])
269 kwt.overwrite(None, expand, status[6])
276 finally:
270 finally:
277 release(lock, wlock)
271 release(lock, wlock)
278
272
279 def demo(ui, repo, *args, **opts):
273 def demo(ui, repo, *args, **opts):
280 '''print [keywordmaps] configuration and an expansion example
274 '''print [keywordmaps] configuration and an expansion example
281
275
282 Show current, custom, or default keyword template maps and their
276 Show current, custom, or default keyword template maps and their
283 expansions.
277 expansions.
284
278
285 Extend current configuration by specifying maps as arguments and
279 Extend current configuration by specifying maps as arguments and
286 optionally by reading from an additional hgrc file.
280 optionally by reading from an additional hgrc file.
287
281
288 Override current keyword template maps with "default" option.
282 Override current keyword template maps with "default" option.
289 '''
283 '''
290 def demostatus(stat):
284 def demostatus(stat):
291 ui.status(_('\n\t%s\n') % stat)
285 ui.status(_('\n\t%s\n') % stat)
292
286
293 def demoitems(section, items):
287 def demoitems(section, items):
294 ui.write('[%s]\n' % section)
288 ui.write('[%s]\n' % section)
295 for k, v in items:
289 for k, v in items:
296 ui.write('%s = %s\n' % (k, v))
290 ui.write('%s = %s\n' % (k, v))
297
291
298 msg = 'hg keyword config and expansion example'
292 msg = 'hg keyword config and expansion example'
299 kwstatus = 'current'
293 kwstatus = 'current'
300 fn = 'demo.txt'
294 fn = 'demo.txt'
301 branchname = 'demobranch'
295 branchname = 'demobranch'
302 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
296 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
303 ui.note(_('creating temporary repository at %s\n') % tmpdir)
297 ui.note(_('creating temporary repository at %s\n') % tmpdir)
304 repo = localrepo.localrepository(ui, tmpdir, True)
298 repo = localrepo.localrepository(ui, tmpdir, True)
305 ui.setconfig('keyword', fn, '')
299 ui.setconfig('keyword', fn, '')
306 if args or opts.get('rcfile'):
300 if args or opts.get('rcfile'):
307 kwstatus = 'custom'
301 kwstatus = 'custom'
308 if opts.get('rcfile'):
302 if opts.get('rcfile'):
309 ui.readconfig(opts.get('rcfile'))
303 ui.readconfig(opts.get('rcfile'))
310 if opts.get('default'):
304 if opts.get('default'):
311 kwstatus = 'default'
305 kwstatus = 'default'
312 kwmaps = kwtemplater.templates
306 kwmaps = kwtemplater.templates
313 if ui.configitems('keywordmaps'):
307 if ui.configitems('keywordmaps'):
314 # override maps from optional rcfile
308 # override maps from optional rcfile
315 for k, v in kwmaps.iteritems():
309 for k, v in kwmaps.iteritems():
316 ui.setconfig('keywordmaps', k, v)
310 ui.setconfig('keywordmaps', k, v)
317 elif args:
311 elif args:
318 # simulate hgrc parsing
312 # simulate hgrc parsing
319 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
313 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
320 fp = repo.opener('hgrc', 'w')
314 fp = repo.opener('hgrc', 'w')
321 fp.writelines(rcmaps)
315 fp.writelines(rcmaps)
322 fp.close()
316 fp.close()
323 ui.readconfig(repo.join('hgrc'))
317 ui.readconfig(repo.join('hgrc'))
324 if not opts.get('default'):
318 if not opts.get('default'):
325 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
319 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
326 uisetup(ui)
320 uisetup(ui)
327 reposetup(ui, repo)
321 reposetup(ui, repo)
328 for k, v in ui.configitems('extensions'):
322 for k, v in ui.configitems('extensions'):
329 if k.endswith('keyword'):
323 if k.endswith('keyword'):
330 extension = '%s = %s' % (k, v)
324 extension = '%s = %s' % (k, v)
331 break
325 break
332 demostatus('config using %s keyword template maps' % kwstatus)
326 demostatus('config using %s keyword template maps' % kwstatus)
333 ui.write('[extensions]\n%s\n' % extension)
327 ui.write('[extensions]\n%s\n' % extension)
334 demoitems('keyword', ui.configitems('keyword'))
328 demoitems('keyword', ui.configitems('keyword'))
335 demoitems('keywordmaps', kwmaps.iteritems())
329 demoitems('keywordmaps', kwmaps.iteritems())
336 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
330 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
337 repo.wopener(fn, 'w').write(keywords)
331 repo.wopener(fn, 'w').write(keywords)
338 repo.add([fn])
332 repo.add([fn])
339 path = repo.wjoin(fn)
333 path = repo.wjoin(fn)
340 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
334 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
341 ui.note(keywords)
335 ui.note(keywords)
342 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
336 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
343 # silence branch command if not verbose
337 # silence branch command if not verbose
344 quiet = ui.quiet
338 quiet = ui.quiet
345 ui.quiet = not ui.verbose
339 ui.quiet = not ui.verbose
346 commands.branch(ui, repo, branchname)
340 commands.branch(ui, repo, branchname)
347 ui.quiet = quiet
341 ui.quiet = quiet
348 for name, cmd in ui.configitems('hooks'):
342 for name, cmd in ui.configitems('hooks'):
349 if name.split('.', 1)[0].find('commit') > -1:
343 if name.split('.', 1)[0].find('commit') > -1:
350 repo.ui.setconfig('hooks', name, '')
344 repo.ui.setconfig('hooks', name, '')
351 ui.note(_('unhooked all commit hooks\n'))
345 ui.note(_('unhooked all commit hooks\n'))
352 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
346 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
353 repo.commit(text=msg)
347 repo.commit(text=msg)
354 fmt = ui.verbose and ' in %s' % path or ''
348 fmt = ui.verbose and ' in %s' % path or ''
355 demostatus('%s keywords expanded%s' % (kwstatus, fmt))
349 demostatus('%s keywords expanded%s' % (kwstatus, fmt))
356 ui.write(repo.wread(fn))
350 ui.write(repo.wread(fn))
357 ui.debug(_('\nremoving temporary repository %s\n') % tmpdir)
351 ui.debug(_('\nremoving temporary repository %s\n') % tmpdir)
358 shutil.rmtree(tmpdir, ignore_errors=True)
352 shutil.rmtree(tmpdir, ignore_errors=True)
359
353
360 def expand(ui, repo, *pats, **opts):
354 def expand(ui, repo, *pats, **opts):
361 '''expand keywords in the working directory
355 '''expand keywords in the working directory
362
356
363 Run after (re)enabling keyword expansion.
357 Run after (re)enabling keyword expansion.
364
358
365 kwexpand refuses to run if given files contain local changes.
359 kwexpand refuses to run if given files contain local changes.
366 '''
360 '''
367 # 3rd argument sets expansion to True
361 # 3rd argument sets expansion to True
368 _kwfwrite(ui, repo, True, *pats, **opts)
362 _kwfwrite(ui, repo, True, *pats, **opts)
369
363
370 def files(ui, repo, *pats, **opts):
364 def files(ui, repo, *pats, **opts):
371 '''print files currently configured for keyword expansion
365 '''print files currently configured for keyword expansion
372
366
373 Crosscheck which files in working directory are potential targets
367 Crosscheck which files in working directory are potential targets
374 for keyword expansion. That is, files matched by [keyword] config
368 for keyword expansion. That is, files matched by [keyword] config
375 patterns but not symlinks.
369 patterns but not symlinks.
376 '''
370 '''
377 kwt = kwtools['templater']
371 kwt = kwtools['templater']
378 status = _status(ui, repo, kwt, opts.get('untracked'), *pats, **opts)
372 status = _status(ui, repo, kwt, opts.get('untracked'), *pats, **opts)
379 modified, added, removed, deleted, unknown, ignored, clean = status
373 modified, added, removed, deleted, unknown, ignored, clean = status
380 files = sorted(modified + added + clean + unknown)
374 files = sorted(modified + added + clean + unknown)
381 wctx = repo[None]
375 wctx = repo[None]
382 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
376 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
383 cwd = pats and repo.getcwd() or ''
377 cwd = pats and repo.getcwd() or ''
384 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
378 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
385 if opts.get('all') or opts.get('ignore'):
379 if opts.get('all') or opts.get('ignore'):
386 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
380 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
387 for char, filenames in kwfstats:
381 for char, filenames in kwfstats:
388 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
382 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
389 for f in filenames:
383 for f in filenames:
390 ui.write(fmt % repo.pathto(f, cwd))
384 ui.write(fmt % repo.pathto(f, cwd))
391
385
392 def shrink(ui, repo, *pats, **opts):
386 def shrink(ui, repo, *pats, **opts):
393 '''revert expanded keywords in the working directory
387 '''revert expanded keywords in the working directory
394
388
395 Run before changing/disabling active keywords or if you experience
389 Run before changing/disabling active keywords or if you experience
396 problems with "hg import" or "hg merge".
390 problems with "hg import" or "hg merge".
397
391
398 kwshrink refuses to run if given files contain local changes.
392 kwshrink refuses to run if given files contain local changes.
399 '''
393 '''
400 # 3rd argument sets expansion to False
394 # 3rd argument sets expansion to False
401 _kwfwrite(ui, repo, False, *pats, **opts)
395 _kwfwrite(ui, repo, False, *pats, **opts)
402
396
403
397
404 def uisetup(ui):
398 def uisetup(ui):
405 '''Collects [keyword] config in kwtools.
399 '''Collects [keyword] config in kwtools.
406 Monkeypatches dispatch._parse if needed.'''
400 Monkeypatches dispatch._parse if needed.'''
407
401
408 for pat, opt in ui.configitems('keyword'):
402 for pat, opt in ui.configitems('keyword'):
409 if opt != 'ignore':
403 if opt != 'ignore':
410 kwtools['inc'].append(pat)
404 kwtools['inc'].append(pat)
411 else:
405 else:
412 kwtools['exc'].append(pat)
406 kwtools['exc'].append(pat)
413
407
414 if kwtools['inc']:
408 if kwtools['inc']:
415 def kwdispatch_parse(orig, ui, args):
409 def kwdispatch_parse(orig, ui, args):
416 '''Monkeypatch dispatch._parse to obtain running hg command.'''
410 '''Monkeypatch dispatch._parse to obtain running hg command.'''
417 cmd, func, args, options, cmdoptions = orig(ui, args)
411 cmd, func, args, options, cmdoptions = orig(ui, args)
418 kwtools['hgcmd'] = cmd
412 kwtools['hgcmd'] = cmd
419 return cmd, func, args, options, cmdoptions
413 return cmd, func, args, options, cmdoptions
420
414
421 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
415 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
422
416
423 def reposetup(ui, repo):
417 def reposetup(ui, repo):
424 '''Sets up repo as kwrepo for keyword substitution.
418 '''Sets up repo as kwrepo for keyword substitution.
425 Overrides file method to return kwfilelog instead of filelog
419 Overrides file method to return kwfilelog instead of filelog
426 if file matches user configuration.
420 if file matches user configuration.
427 Wraps commit to overwrite configured files with updated
421 Wraps commit to overwrite configured files with updated
428 keyword substitutions.
422 keyword substitutions.
429 Monkeypatches patch and webcommands.'''
423 Monkeypatches patch and webcommands.'''
430
424
431 try:
425 try:
432 if (not repo.local() or not kwtools['inc']
426 if (not repo.local() or not kwtools['inc']
433 or kwtools['hgcmd'] in nokwcommands.split()
427 or kwtools['hgcmd'] in nokwcommands.split()
434 or '.hg' in util.splitpath(repo.root)
428 or '.hg' in util.splitpath(repo.root)
435 or repo._url.startswith('bundle:')):
429 or repo._url.startswith('bundle:')):
436 return
430 return
437 except AttributeError:
431 except AttributeError:
438 pass
432 pass
439
433
440 kwtools['templater'] = kwt = kwtemplater(ui, repo)
434 kwtools['templater'] = kwt = kwtemplater(ui, repo)
441
435
442 class kwrepo(repo.__class__):
436 class kwrepo(repo.__class__):
443 def file(self, f):
437 def file(self, f):
444 if f[0] == '/':
438 if f[0] == '/':
445 f = f[1:]
439 f = f[1:]
446 return kwfilelog(self.sopener, kwt, f)
440 return kwfilelog(self.sopener, kwt, f)
447
441
448 def wread(self, filename):
442 def wread(self, filename):
449 data = super(kwrepo, self).wread(filename)
443 data = super(kwrepo, self).wread(filename)
450 return kwt.wread(filename, data)
444 return kwt.wread(filename, data)
451
445
452 def commit(self, text='', user=None, date=None, match=None,
446 def commit(self, text='', user=None, date=None, match=None,
453 force=False, editor=None, extra={}):
447 force=False, editor=None, extra={}):
454 wlock = lock = None
448 wlock = lock = None
455 _p1 = _p2 = None
449 _p1 = _p2 = None
456 try:
450 try:
457 wlock = self.wlock()
451 wlock = self.wlock()
458 lock = self.lock()
452 lock = self.lock()
459 # store and postpone commit hooks
453 # store and postpone commit hooks
460 commithooks = {}
454 commithooks = {}
461 for name, cmd in ui.configitems('hooks'):
455 for name, cmd in ui.configitems('hooks'):
462 if name.split('.', 1)[0] == 'commit':
456 if name.split('.', 1)[0] == 'commit':
463 commithooks[name] = cmd
457 commithooks[name] = cmd
464 ui.setconfig('hooks', name, None)
458 ui.setconfig('hooks', name, None)
465 if commithooks:
459 if commithooks:
466 # store parents for commit hook environment
460 # store parents for commit hook environment
467 _p1, _p2 = repo.dirstate.parents()
461 _p1, _p2 = repo.dirstate.parents()
468 _p1 = hex(_p1)
462 _p1 = hex(_p1)
469 if _p2 == nullid:
463 if _p2 == nullid:
470 _p2 = ''
464 _p2 = ''
471 else:
465 else:
472 _p2 = hex(_p2)
466 _p2 = hex(_p2)
473
467
474 n = super(kwrepo, self).commit(text, user, date, match, force,
468 n = super(kwrepo, self).commit(text, user, date, match, force,
475 editor, extra)
469 editor, extra)
476
470
477 # restore commit hooks
471 # restore commit hooks
478 for name, cmd in commithooks.iteritems():
472 for name, cmd in commithooks.iteritems():
479 ui.setconfig('hooks', name, cmd)
473 ui.setconfig('hooks', name, cmd)
480 if n is not None:
474 if n is not None:
481 kwt.overwrite(n, True, None)
475 kwt.overwrite(n, True, None)
482 repo.hook('commit', node=n, parent1=_p1, parent2=_p2)
476 repo.hook('commit', node=n, parent1=_p1, parent2=_p2)
483 return n
477 return n
484 finally:
478 finally:
485 release(lock, wlock)
479 release(lock, wlock)
486
480
487 # monkeypatches
481 # monkeypatches
488 def kwpatchfile_init(orig, self, ui, fname, opener, missing=False, eol=None):
482 def kwpatchfile_init(orig, self, ui, fname, opener, missing=False, eol=None):
489 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
483 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
490 rejects or conflicts due to expanded keywords in working dir.'''
484 rejects or conflicts due to expanded keywords in working dir.'''
491 orig(self, ui, fname, opener, missing, eol)
485 orig(self, ui, fname, opener, missing, eol)
492 # shrink keywords read from working dir
486 # shrink keywords read from working dir
493 self.lines = kwt.shrinklines(self.fname, self.lines)
487 self.lines = kwt.shrinklines(self.fname, self.lines)
494
488
495 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
489 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
496 opts=None):
490 opts=None):
497 '''Monkeypatch patch.diff to avoid expansion except when
491 '''Monkeypatch patch.diff to avoid expansion except when
498 comparing against working dir.'''
492 comparing against working dir.'''
499 if node2 is not None:
493 if node2 is not None:
500 kwt.match = util.never
494 kwt.match = util.never
501 elif node1 is not None and node1 != repo['.'].node():
495 elif node1 is not None and node1 != repo['.'].node():
502 kwt.restrict = True
496 kwt.restrict = True
503 return orig(repo, node1, node2, match, changes, opts)
497 return orig(repo, node1, node2, match, changes, opts)
504
498
505 def kwweb_skip(orig, web, req, tmpl):
499 def kwweb_skip(orig, web, req, tmpl):
506 '''Wraps webcommands.x turning off keyword expansion.'''
500 '''Wraps webcommands.x turning off keyword expansion.'''
507 kwt.match = util.never
501 kwt.match = util.never
508 return orig(web, req, tmpl)
502 return orig(web, req, tmpl)
509
503
510 repo.__class__ = kwrepo
504 repo.__class__ = kwrepo
511
505
512 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
506 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
513 extensions.wrapfunction(patch, 'diff', kw_diff)
507 extensions.wrapfunction(patch, 'diff', kw_diff)
514 for c in 'annotate changeset rev filediff diff'.split():
508 for c in 'annotate changeset rev filediff diff'.split():
515 extensions.wrapfunction(webcommands, c, kwweb_skip)
509 extensions.wrapfunction(webcommands, c, kwweb_skip)
516
510
517 cmdtable = {
511 cmdtable = {
518 'kwdemo':
512 'kwdemo':
519 (demo,
513 (demo,
520 [('d', 'default', None, _('show default keyword template maps')),
514 [('d', 'default', None, _('show default keyword template maps')),
521 ('f', 'rcfile', [], _('read maps from rcfile'))],
515 ('f', 'rcfile', [], _('read maps from rcfile'))],
522 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
516 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
523 'kwexpand': (expand, commands.walkopts,
517 'kwexpand': (expand, commands.walkopts,
524 _('hg kwexpand [OPTION]... [FILE]...')),
518 _('hg kwexpand [OPTION]... [FILE]...')),
525 'kwfiles':
519 'kwfiles':
526 (files,
520 (files,
527 [('a', 'all', None, _('show keyword status flags of all files')),
521 [('a', 'all', None, _('show keyword status flags of all files')),
528 ('i', 'ignore', None, _('show files excluded from expansion')),
522 ('i', 'ignore', None, _('show files excluded from expansion')),
529 ('u', 'untracked', None, _('additionally show untracked files')),
523 ('u', 'untracked', None, _('additionally show untracked files')),
530 ] + commands.walkopts,
524 ] + commands.walkopts,
531 _('hg kwfiles [OPTION]... [FILE]...')),
525 _('hg kwfiles [OPTION]... [FILE]...')),
532 'kwshrink': (shrink, commands.walkopts,
526 'kwshrink': (shrink, commands.walkopts,
533 _('hg kwshrink [OPTION]... [FILE]...')),
527 _('hg kwshrink [OPTION]... [FILE]...')),
534 }
528 }
@@ -1,512 +1,507 b''
1 # patchbomb.py - sending Mercurial changesets as patch emails
1 # patchbomb.py - sending Mercurial changesets as patch emails
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''sending Mercurial changesets as a series of patch emails
8 '''sending Mercurial changesets as a series of patch emails
9
9
10 The series is started off with a "[PATCH 0 of N]" introduction, which
10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 describes the series as a whole.
11 describes the series as a whole.
12
12
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 first line of the changeset description as the subject text. The
14 first line of the changeset description as the subject text. The
15 message contains two or three body parts:
15 message contains two or three body parts:
16
16
17 The changeset description.
17 The changeset description.
18
18
19 [Optional] The result of running diffstat on the patch.
19 [Optional] The result of running diffstat on the patch.
20
20
21 The patch itself, as generated by "hg export".
21 The patch itself, as generated by "hg export".
22
22
23 Each message refers to the first in the series using the In-Reply-To
23 Each message refers to the first in the series using the In-Reply-To
24 and References headers, so they will show up as a sequence in threaded
24 and References headers, so they will show up as a sequence in threaded
25 mail and news readers, and in mail archives.
25 mail and news readers, and in mail archives.
26
26
27 With the -d/--diffstat option, you will be prompted for each changeset
27 With the -d/--diffstat option, you will be prompted for each changeset
28 with a diffstat summary and the changeset summary, so you can be sure
28 with a diffstat summary and the changeset summary, so you can be sure
29 you are sending the right changes.
29 you are sending the right changes.
30
30
31 To enable this extension:
32
33 [extensions]
34 hgext.patchbomb =
35
36 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
37 file:
32 file:
38
33
39 [email]
34 [email]
40 from = My Name <my@email>
35 from = My Name <my@email>
41 to = recipient1, recipient2, ...
36 to = recipient1, recipient2, ...
42 cc = cc1, cc2, ...
37 cc = cc1, cc2, ...
43 bcc = bcc1, bcc2, ...
38 bcc = bcc1, bcc2, ...
44
39
45 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
46 as a patchbomb.
41 as a patchbomb.
47
42
48 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
49 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
50 prompted for an email recipient address, a subject and an introductory
45 prompted for an email recipient address, a subject and an introductory
51 message describing the patches of your patchbomb. Then when all is
46 message describing the patches of your patchbomb. Then when all is
52 done, patchbomb messages are displayed. If the PAGER environment
47 done, patchbomb messages are displayed. If the PAGER environment
53 variable is set, your pager will be fired up once for each patchbomb
48 variable is set, your pager will be fired up once for each patchbomb
54 message, so you can verify everything is alright.
49 message, so you can verify everything is alright.
55
50
56 The -m/--mbox option is also very useful. Instead of previewing each
51 The -m/--mbox option is also very useful. Instead of previewing each
57 patchbomb message in a pager or sending the messages directly, it will
52 patchbomb message in a pager or sending the messages directly, it will
58 create a UNIX mailbox file with the patch emails. This mailbox file
53 create a UNIX mailbox file with the patch emails. This mailbox file
59 can be previewed with any mail user agent which supports UNIX mbox
54 can be previewed with any mail user agent which supports UNIX mbox
60 files, e.g. with mutt:
55 files, e.g. with mutt:
61
56
62 % mutt -R -f mbox
57 % mutt -R -f mbox
63
58
64 When you are previewing the patchbomb messages, you can use `formail'
59 When you are previewing the patchbomb messages, you can use `formail'
65 (a utility that is commonly installed as part of the procmail
60 (a utility that is commonly installed as part of the procmail
66 package), to send each message out:
61 package), to send each message out:
67
62
68 % formail -s sendmail -bm -t < mbox
63 % formail -s sendmail -bm -t < mbox
69
64
70 That should be all. Now your patchbomb is on its way out.
65 That should be all. Now your patchbomb is on its way out.
71
66
72 You can also either configure the method option in the email section
67 You can also either configure the method option in the email section
73 to be a sendmail compatible mailer or fill out the [smtp] section so
68 to be a sendmail compatible mailer or fill out the [smtp] section so
74 that the patchbomb extension can automatically send patchbombs
69 that the patchbomb extension can automatically send patchbombs
75 directly from the commandline. See the [email] and [smtp] sections in
70 directly from the commandline. See the [email] and [smtp] sections in
76 hgrc(5) for details.'''
71 hgrc(5) for details.'''
77
72
78 import os, errno, socket, tempfile, cStringIO
73 import os, errno, socket, tempfile, cStringIO
79 import email.MIMEMultipart, email.MIMEBase
74 import email.MIMEMultipart, email.MIMEBase
80 import email.Utils, email.Encoders, email.Generator
75 import email.Utils, email.Encoders, email.Generator
81 from mercurial import cmdutil, commands, hg, mail, patch, util
76 from mercurial import cmdutil, commands, hg, mail, patch, util
82 from mercurial.i18n import _
77 from mercurial.i18n import _
83 from mercurial.node import bin
78 from mercurial.node import bin
84
79
85 def prompt(ui, prompt, default=None, rest=': ', empty_ok=False):
80 def prompt(ui, prompt, default=None, rest=': ', empty_ok=False):
86 if not ui.interactive():
81 if not ui.interactive():
87 return default
82 return default
88 if default:
83 if default:
89 prompt += ' [%s]' % default
84 prompt += ' [%s]' % default
90 prompt += rest
85 prompt += rest
91 while True:
86 while True:
92 r = ui.prompt(prompt, default=default)
87 r = ui.prompt(prompt, default=default)
93 if r:
88 if r:
94 return r
89 return r
95 if default is not None:
90 if default is not None:
96 return default
91 return default
97 if empty_ok:
92 if empty_ok:
98 return r
93 return r
99 ui.warn(_('Please enter a valid value.\n'))
94 ui.warn(_('Please enter a valid value.\n'))
100
95
101 def cdiffstat(ui, summary, patchlines):
96 def cdiffstat(ui, summary, patchlines):
102 s = patch.diffstat(patchlines)
97 s = patch.diffstat(patchlines)
103 if summary:
98 if summary:
104 ui.write(summary, '\n')
99 ui.write(summary, '\n')
105 ui.write(s, '\n')
100 ui.write(s, '\n')
106 ans = prompt(ui, _('does the diffstat above look okay? '), 'y')
101 ans = prompt(ui, _('does the diffstat above look okay? '), 'y')
107 if not ans.lower().startswith('y'):
102 if not ans.lower().startswith('y'):
108 raise util.Abort(_('diffstat rejected'))
103 raise util.Abort(_('diffstat rejected'))
109 return s
104 return s
110
105
111 def makepatch(ui, repo, patch, opts, _charsets, idx, total, patchname=None):
106 def makepatch(ui, repo, patch, opts, _charsets, idx, total, patchname=None):
112
107
113 desc = []
108 desc = []
114 node = None
109 node = None
115 body = ''
110 body = ''
116
111
117 for line in patch:
112 for line in patch:
118 if line.startswith('#'):
113 if line.startswith('#'):
119 if line.startswith('# Node ID'):
114 if line.startswith('# Node ID'):
120 node = line.split()[-1]
115 node = line.split()[-1]
121 continue
116 continue
122 if line.startswith('diff -r') or line.startswith('diff --git'):
117 if line.startswith('diff -r') or line.startswith('diff --git'):
123 break
118 break
124 desc.append(line)
119 desc.append(line)
125
120
126 if not patchname and not node:
121 if not patchname and not node:
127 raise ValueError
122 raise ValueError
128
123
129 if opts.get('attach'):
124 if opts.get('attach'):
130 body = ('\n'.join(desc[1:]).strip() or
125 body = ('\n'.join(desc[1:]).strip() or
131 'Patch subject is complete summary.')
126 'Patch subject is complete summary.')
132 body += '\n\n\n'
127 body += '\n\n\n'
133
128
134 if opts.get('plain'):
129 if opts.get('plain'):
135 while patch and patch[0].startswith('# '):
130 while patch and patch[0].startswith('# '):
136 patch.pop(0)
131 patch.pop(0)
137 if patch:
132 if patch:
138 patch.pop(0)
133 patch.pop(0)
139 while patch and not patch[0].strip():
134 while patch and not patch[0].strip():
140 patch.pop(0)
135 patch.pop(0)
141
136
142 if opts.get('diffstat'):
137 if opts.get('diffstat'):
143 body += cdiffstat(ui, '\n'.join(desc), patch) + '\n\n'
138 body += cdiffstat(ui, '\n'.join(desc), patch) + '\n\n'
144
139
145 if opts.get('attach') or opts.get('inline'):
140 if opts.get('attach') or opts.get('inline'):
146 msg = email.MIMEMultipart.MIMEMultipart()
141 msg = email.MIMEMultipart.MIMEMultipart()
147 if body:
142 if body:
148 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
143 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
149 p = mail.mimetextpatch('\n'.join(patch), 'x-patch', opts.get('test'))
144 p = mail.mimetextpatch('\n'.join(patch), 'x-patch', opts.get('test'))
150 binnode = bin(node)
145 binnode = bin(node)
151 # if node is mq patch, it will have the patch file's name as a tag
146 # if node is mq patch, it will have the patch file's name as a tag
152 if not patchname:
147 if not patchname:
153 patchtags = [t for t in repo.nodetags(binnode)
148 patchtags = [t for t in repo.nodetags(binnode)
154 if t.endswith('.patch') or t.endswith('.diff')]
149 if t.endswith('.patch') or t.endswith('.diff')]
155 if patchtags:
150 if patchtags:
156 patchname = patchtags[0]
151 patchname = patchtags[0]
157 elif total > 1:
152 elif total > 1:
158 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
153 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
159 binnode, seqno=idx, total=total)
154 binnode, seqno=idx, total=total)
160 else:
155 else:
161 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
156 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
162 disposition = 'inline'
157 disposition = 'inline'
163 if opts.get('attach'):
158 if opts.get('attach'):
164 disposition = 'attachment'
159 disposition = 'attachment'
165 p['Content-Disposition'] = disposition + '; filename=' + patchname
160 p['Content-Disposition'] = disposition + '; filename=' + patchname
166 msg.attach(p)
161 msg.attach(p)
167 else:
162 else:
168 body += '\n'.join(patch)
163 body += '\n'.join(patch)
169 msg = mail.mimetextpatch(body, display=opts.get('test'))
164 msg = mail.mimetextpatch(body, display=opts.get('test'))
170
165
171 subj = desc[0].strip().rstrip('. ')
166 subj = desc[0].strip().rstrip('. ')
172 if total == 1 and not opts.get('intro'):
167 if total == 1 and not opts.get('intro'):
173 subj = '[PATCH] ' + (opts.get('subject') or subj)
168 subj = '[PATCH] ' + (opts.get('subject') or subj)
174 else:
169 else:
175 tlen = len(str(total))
170 tlen = len(str(total))
176 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
171 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
177 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
172 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
178 msg['X-Mercurial-Node'] = node
173 msg['X-Mercurial-Node'] = node
179 return msg, subj
174 return msg, subj
180
175
181 def patchbomb(ui, repo, *revs, **opts):
176 def patchbomb(ui, repo, *revs, **opts):
182 '''send changesets by email
177 '''send changesets by email
183
178
184 By default, diffs are sent in the format generated by hg export,
179 By default, diffs are sent in the format generated by hg export,
185 one per message. The series starts with a "[PATCH 0 of N]"
180 one per message. The series starts with a "[PATCH 0 of N]"
186 introduction, which describes the series as a whole.
181 introduction, which describes the series as a whole.
187
182
188 Each patch email has a Subject line of "[PATCH M of N] ...", using
183 Each patch email has a Subject line of "[PATCH M of N] ...", using
189 the first line of the changeset description as the subject text.
184 the first line of the changeset description as the subject text.
190 The message contains two or three parts. First, the changeset
185 The message contains two or three parts. First, the changeset
191 description. Next, (optionally) if the diffstat program is
186 description. Next, (optionally) if the diffstat program is
192 installed and -d/--diffstat is used, the result of running
187 installed and -d/--diffstat is used, the result of running
193 diffstat on the patch. Finally, the patch itself, as generated by
188 diffstat on the patch. Finally, the patch itself, as generated by
194 "hg export".
189 "hg export".
195
190
196 By default the patch is included as text in the email body for
191 By default the patch is included as text in the email body for
197 easy reviewing. Using the -a/--attach option will instead create
192 easy reviewing. Using the -a/--attach option will instead create
198 an attachment for the patch. With -i/--inline an inline attachment
193 an attachment for the patch. With -i/--inline an inline attachment
199 will be created.
194 will be created.
200
195
201 With -o/--outgoing, emails will be generated for patches not found
196 With -o/--outgoing, emails will be generated for patches not found
202 in the destination repository (or only those which are ancestors
197 in the destination repository (or only those which are ancestors
203 of the specified revisions if any are provided)
198 of the specified revisions if any are provided)
204
199
205 With -b/--bundle, changesets are selected as for --outgoing, but a
200 With -b/--bundle, changesets are selected as for --outgoing, but a
206 single email containing a binary Mercurial bundle as an attachment
201 single email containing a binary Mercurial bundle as an attachment
207 will be sent.
202 will be sent.
208
203
209 Examples:
204 Examples:
210
205
211 hg email -r 3000 # send patch 3000 only
206 hg email -r 3000 # send patch 3000 only
212 hg email -r 3000 -r 3001 # send patches 3000 and 3001
207 hg email -r 3000 -r 3001 # send patches 3000 and 3001
213 hg email -r 3000:3005 # send patches 3000 through 3005
208 hg email -r 3000:3005 # send patches 3000 through 3005
214 hg email 3000 # send patch 3000 (deprecated)
209 hg email 3000 # send patch 3000 (deprecated)
215
210
216 hg email -o # send all patches not in default
211 hg email -o # send all patches not in default
217 hg email -o DEST # send all patches not in DEST
212 hg email -o DEST # send all patches not in DEST
218 hg email -o -r 3000 # send all ancestors of 3000 not in default
213 hg email -o -r 3000 # send all ancestors of 3000 not in default
219 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
214 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
220
215
221 hg email -b # send bundle of all patches not in default
216 hg email -b # send bundle of all patches not in default
222 hg email -b DEST # send bundle of all patches not in DEST
217 hg email -b DEST # send bundle of all patches not in DEST
223 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
218 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
224 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
219 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
225
220
226 Before using this command, you will need to enable email in your
221 Before using this command, you will need to enable email in your
227 hgrc. See the [email] section in hgrc(5) for details.
222 hgrc. See the [email] section in hgrc(5) for details.
228 '''
223 '''
229
224
230 _charsets = mail._charsets(ui)
225 _charsets = mail._charsets(ui)
231
226
232 def outgoing(dest, revs):
227 def outgoing(dest, revs):
233 '''Return the revisions present locally but not in dest'''
228 '''Return the revisions present locally but not in dest'''
234 dest = ui.expandpath(dest or 'default-push', dest or 'default')
229 dest = ui.expandpath(dest or 'default-push', dest or 'default')
235 revs = [repo.lookup(rev) for rev in revs]
230 revs = [repo.lookup(rev) for rev in revs]
236 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
231 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
237 ui.status(_('comparing with %s\n') % dest)
232 ui.status(_('comparing with %s\n') % dest)
238 o = repo.findoutgoing(other)
233 o = repo.findoutgoing(other)
239 if not o:
234 if not o:
240 ui.status(_("no changes found\n"))
235 ui.status(_("no changes found\n"))
241 return []
236 return []
242 o = repo.changelog.nodesbetween(o, revs or None)[0]
237 o = repo.changelog.nodesbetween(o, revs or None)[0]
243 return [str(repo.changelog.rev(r)) for r in o]
238 return [str(repo.changelog.rev(r)) for r in o]
244
239
245 def getpatches(revs):
240 def getpatches(revs):
246 for r in cmdutil.revrange(repo, revs):
241 for r in cmdutil.revrange(repo, revs):
247 output = cStringIO.StringIO()
242 output = cStringIO.StringIO()
248 patch.export(repo, [r], fp=output,
243 patch.export(repo, [r], fp=output,
249 opts=patch.diffopts(ui, opts))
244 opts=patch.diffopts(ui, opts))
250 yield output.getvalue().split('\n')
245 yield output.getvalue().split('\n')
251
246
252 def getbundle(dest):
247 def getbundle(dest):
253 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
248 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
254 tmpfn = os.path.join(tmpdir, 'bundle')
249 tmpfn = os.path.join(tmpdir, 'bundle')
255 try:
250 try:
256 commands.bundle(ui, repo, tmpfn, dest, **opts)
251 commands.bundle(ui, repo, tmpfn, dest, **opts)
257 return open(tmpfn, 'rb').read()
252 return open(tmpfn, 'rb').read()
258 finally:
253 finally:
259 try:
254 try:
260 os.unlink(tmpfn)
255 os.unlink(tmpfn)
261 except:
256 except:
262 pass
257 pass
263 os.rmdir(tmpdir)
258 os.rmdir(tmpdir)
264
259
265 if not (opts.get('test') or opts.get('mbox')):
260 if not (opts.get('test') or opts.get('mbox')):
266 # really sending
261 # really sending
267 mail.validateconfig(ui)
262 mail.validateconfig(ui)
268
263
269 if not (revs or opts.get('rev')
264 if not (revs or opts.get('rev')
270 or opts.get('outgoing') or opts.get('bundle')
265 or opts.get('outgoing') or opts.get('bundle')
271 or opts.get('patches')):
266 or opts.get('patches')):
272 raise util.Abort(_('specify at least one changeset with -r or -o'))
267 raise util.Abort(_('specify at least one changeset with -r or -o'))
273
268
274 if opts.get('outgoing') and opts.get('bundle'):
269 if opts.get('outgoing') and opts.get('bundle'):
275 raise util.Abort(_("--outgoing mode always on with --bundle;"
270 raise util.Abort(_("--outgoing mode always on with --bundle;"
276 " do not re-specify --outgoing"))
271 " do not re-specify --outgoing"))
277
272
278 if opts.get('outgoing') or opts.get('bundle'):
273 if opts.get('outgoing') or opts.get('bundle'):
279 if len(revs) > 1:
274 if len(revs) > 1:
280 raise util.Abort(_("too many destinations"))
275 raise util.Abort(_("too many destinations"))
281 dest = revs and revs[0] or None
276 dest = revs and revs[0] or None
282 revs = []
277 revs = []
283
278
284 if opts.get('rev'):
279 if opts.get('rev'):
285 if revs:
280 if revs:
286 raise util.Abort(_('use only one form to specify the revision'))
281 raise util.Abort(_('use only one form to specify the revision'))
287 revs = opts.get('rev')
282 revs = opts.get('rev')
288
283
289 if opts.get('outgoing'):
284 if opts.get('outgoing'):
290 revs = outgoing(dest, opts.get('rev'))
285 revs = outgoing(dest, opts.get('rev'))
291 if opts.get('bundle'):
286 if opts.get('bundle'):
292 opts['revs'] = revs
287 opts['revs'] = revs
293
288
294 # start
289 # start
295 if opts.get('date'):
290 if opts.get('date'):
296 start_time = util.parsedate(opts.get('date'))
291 start_time = util.parsedate(opts.get('date'))
297 else:
292 else:
298 start_time = util.makedate()
293 start_time = util.makedate()
299
294
300 def genmsgid(id):
295 def genmsgid(id):
301 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
296 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
302
297
303 def getdescription(body, sender):
298 def getdescription(body, sender):
304 if opts.get('desc'):
299 if opts.get('desc'):
305 body = open(opts.get('desc')).read()
300 body = open(opts.get('desc')).read()
306 else:
301 else:
307 ui.write(_('\nWrite the introductory message for the '
302 ui.write(_('\nWrite the introductory message for the '
308 'patch series.\n\n'))
303 'patch series.\n\n'))
309 body = ui.edit(body, sender)
304 body = ui.edit(body, sender)
310 return body
305 return body
311
306
312 def getpatchmsgs(patches, patchnames=None):
307 def getpatchmsgs(patches, patchnames=None):
313 jumbo = []
308 jumbo = []
314 msgs = []
309 msgs = []
315
310
316 ui.write(_('This patch series consists of %d patches.\n\n')
311 ui.write(_('This patch series consists of %d patches.\n\n')
317 % len(patches))
312 % len(patches))
318
313
319 name = None
314 name = None
320 for i, p in enumerate(patches):
315 for i, p in enumerate(patches):
321 jumbo.extend(p)
316 jumbo.extend(p)
322 if patchnames:
317 if patchnames:
323 name = patchnames[i]
318 name = patchnames[i]
324 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
319 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
325 len(patches), name)
320 len(patches), name)
326 msgs.append(msg)
321 msgs.append(msg)
327
322
328 if len(patches) > 1 or opts.get('intro'):
323 if len(patches) > 1 or opts.get('intro'):
329 tlen = len(str(len(patches)))
324 tlen = len(str(len(patches)))
330
325
331 subj = '[PATCH %0*d of %d] %s' % (
326 subj = '[PATCH %0*d of %d] %s' % (
332 tlen, 0, len(patches),
327 tlen, 0, len(patches),
333 opts.get('subject') or
328 opts.get('subject') or
334 prompt(ui, 'Subject:',
329 prompt(ui, 'Subject:',
335 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
330 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
336
331
337 body = ''
332 body = ''
338 if opts.get('diffstat'):
333 if opts.get('diffstat'):
339 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
334 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
340 if d:
335 if d:
341 body = '\n' + d
336 body = '\n' + d
342
337
343 body = getdescription(body, sender)
338 body = getdescription(body, sender)
344 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
339 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
345 msg['Subject'] = mail.headencode(ui, subj, _charsets,
340 msg['Subject'] = mail.headencode(ui, subj, _charsets,
346 opts.get('test'))
341 opts.get('test'))
347
342
348 msgs.insert(0, (msg, subj))
343 msgs.insert(0, (msg, subj))
349 return msgs
344 return msgs
350
345
351 def getbundlemsgs(bundle):
346 def getbundlemsgs(bundle):
352 subj = (opts.get('subject')
347 subj = (opts.get('subject')
353 or prompt(ui, 'Subject:', 'A bundle for your repository'))
348 or prompt(ui, 'Subject:', 'A bundle for your repository'))
354
349
355 body = getdescription('', sender)
350 body = getdescription('', sender)
356 msg = email.MIMEMultipart.MIMEMultipart()
351 msg = email.MIMEMultipart.MIMEMultipart()
357 if body:
352 if body:
358 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
353 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
359 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
354 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
360 datapart.set_payload(bundle)
355 datapart.set_payload(bundle)
361 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
356 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
362 datapart.add_header('Content-Disposition', 'attachment',
357 datapart.add_header('Content-Disposition', 'attachment',
363 filename=bundlename)
358 filename=bundlename)
364 email.Encoders.encode_base64(datapart)
359 email.Encoders.encode_base64(datapart)
365 msg.attach(datapart)
360 msg.attach(datapart)
366 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
361 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
367 return [(msg, subj)]
362 return [(msg, subj)]
368
363
369 sender = (opts.get('from') or ui.config('email', 'from') or
364 sender = (opts.get('from') or ui.config('email', 'from') or
370 ui.config('patchbomb', 'from') or
365 ui.config('patchbomb', 'from') or
371 prompt(ui, 'From', ui.username()))
366 prompt(ui, 'From', ui.username()))
372
367
373 # internal option used by pbranches
368 # internal option used by pbranches
374 patches = opts.get('patches')
369 patches = opts.get('patches')
375 if patches:
370 if patches:
376 msgs = getpatchmsgs(patches, opts.get('patchnames'))
371 msgs = getpatchmsgs(patches, opts.get('patchnames'))
377 elif opts.get('bundle'):
372 elif opts.get('bundle'):
378 msgs = getbundlemsgs(getbundle(dest))
373 msgs = getbundlemsgs(getbundle(dest))
379 else:
374 else:
380 msgs = getpatchmsgs(list(getpatches(revs)))
375 msgs = getpatchmsgs(list(getpatches(revs)))
381
376
382 def getaddrs(opt, prpt, default = None):
377 def getaddrs(opt, prpt, default = None):
383 addrs = opts.get(opt) or (ui.config('email', opt) or
378 addrs = opts.get(opt) or (ui.config('email', opt) or
384 ui.config('patchbomb', opt) or
379 ui.config('patchbomb', opt) or
385 prompt(ui, prpt, default)).split(',')
380 prompt(ui, prpt, default)).split(',')
386 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
381 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
387 for a in addrs if a.strip()]
382 for a in addrs if a.strip()]
388
383
389 to = getaddrs('to', 'To')
384 to = getaddrs('to', 'To')
390 cc = getaddrs('cc', 'Cc', '')
385 cc = getaddrs('cc', 'Cc', '')
391
386
392 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
387 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
393 ui.config('patchbomb', 'bcc') or '').split(',')
388 ui.config('patchbomb', 'bcc') or '').split(',')
394 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
389 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
395 for a in bcc if a.strip()]
390 for a in bcc if a.strip()]
396
391
397 ui.write('\n')
392 ui.write('\n')
398
393
399 parent = opts.get('in_reply_to') or None
394 parent = opts.get('in_reply_to') or None
400 # angle brackets may be omitted, they're not semantically part of the msg-id
395 # angle brackets may be omitted, they're not semantically part of the msg-id
401 if parent is not None:
396 if parent is not None:
402 if not parent.startswith('<'):
397 if not parent.startswith('<'):
403 parent = '<' + parent
398 parent = '<' + parent
404 if not parent.endswith('>'):
399 if not parent.endswith('>'):
405 parent += '>'
400 parent += '>'
406
401
407 first = True
402 first = True
408
403
409 sender_addr = email.Utils.parseaddr(sender)[1]
404 sender_addr = email.Utils.parseaddr(sender)[1]
410 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
405 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
411 sendmail = None
406 sendmail = None
412 for m, subj in msgs:
407 for m, subj in msgs:
413 try:
408 try:
414 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
409 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
415 except TypeError:
410 except TypeError:
416 m['Message-Id'] = genmsgid('patchbomb')
411 m['Message-Id'] = genmsgid('patchbomb')
417 if parent:
412 if parent:
418 m['In-Reply-To'] = parent
413 m['In-Reply-To'] = parent
419 m['References'] = parent
414 m['References'] = parent
420 if first:
415 if first:
421 parent = m['Message-Id']
416 parent = m['Message-Id']
422 first = False
417 first = False
423
418
424 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
419 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
425 m['Date'] = email.Utils.formatdate(start_time[0])
420 m['Date'] = email.Utils.formatdate(start_time[0])
426
421
427 start_time = (start_time[0] + 1, start_time[1])
422 start_time = (start_time[0] + 1, start_time[1])
428 m['From'] = sender
423 m['From'] = sender
429 m['To'] = ', '.join(to)
424 m['To'] = ', '.join(to)
430 if cc:
425 if cc:
431 m['Cc'] = ', '.join(cc)
426 m['Cc'] = ', '.join(cc)
432 if bcc:
427 if bcc:
433 m['Bcc'] = ', '.join(bcc)
428 m['Bcc'] = ', '.join(bcc)
434 if opts.get('test'):
429 if opts.get('test'):
435 ui.status(_('Displaying '), subj, ' ...\n')
430 ui.status(_('Displaying '), subj, ' ...\n')
436 ui.flush()
431 ui.flush()
437 if 'PAGER' in os.environ:
432 if 'PAGER' in os.environ:
438 fp = util.popen(os.environ['PAGER'], 'w')
433 fp = util.popen(os.environ['PAGER'], 'w')
439 else:
434 else:
440 fp = ui
435 fp = ui
441 generator = email.Generator.Generator(fp, mangle_from_=False)
436 generator = email.Generator.Generator(fp, mangle_from_=False)
442 try:
437 try:
443 generator.flatten(m, 0)
438 generator.flatten(m, 0)
444 fp.write('\n')
439 fp.write('\n')
445 except IOError, inst:
440 except IOError, inst:
446 if inst.errno != errno.EPIPE:
441 if inst.errno != errno.EPIPE:
447 raise
442 raise
448 if fp is not ui:
443 if fp is not ui:
449 fp.close()
444 fp.close()
450 elif opts.get('mbox'):
445 elif opts.get('mbox'):
451 ui.status(_('Writing '), subj, ' ...\n')
446 ui.status(_('Writing '), subj, ' ...\n')
452 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
447 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
453 generator = email.Generator.Generator(fp, mangle_from_=True)
448 generator = email.Generator.Generator(fp, mangle_from_=True)
454 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
449 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
455 fp.write('From %s %s\n' % (sender_addr, date))
450 fp.write('From %s %s\n' % (sender_addr, date))
456 generator.flatten(m, 0)
451 generator.flatten(m, 0)
457 fp.write('\n\n')
452 fp.write('\n\n')
458 fp.close()
453 fp.close()
459 else:
454 else:
460 if not sendmail:
455 if not sendmail:
461 sendmail = mail.connect(ui)
456 sendmail = mail.connect(ui)
462 ui.status(_('Sending '), subj, ' ...\n')
457 ui.status(_('Sending '), subj, ' ...\n')
463 # Exim does not remove the Bcc field
458 # Exim does not remove the Bcc field
464 del m['Bcc']
459 del m['Bcc']
465 fp = cStringIO.StringIO()
460 fp = cStringIO.StringIO()
466 generator = email.Generator.Generator(fp, mangle_from_=False)
461 generator = email.Generator.Generator(fp, mangle_from_=False)
467 generator.flatten(m, 0)
462 generator.flatten(m, 0)
468 sendmail(sender, to + bcc + cc, fp.getvalue())
463 sendmail(sender, to + bcc + cc, fp.getvalue())
469
464
470 emailopts = [
465 emailopts = [
471 ('a', 'attach', None, _('send patches as attachments')),
466 ('a', 'attach', None, _('send patches as attachments')),
472 ('i', 'inline', None, _('send patches as inline attachments')),
467 ('i', 'inline', None, _('send patches as inline attachments')),
473 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
468 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
474 ('c', 'cc', [], _('email addresses of copy recipients')),
469 ('c', 'cc', [], _('email addresses of copy recipients')),
475 ('d', 'diffstat', None, _('add diffstat output to messages')),
470 ('d', 'diffstat', None, _('add diffstat output to messages')),
476 ('', 'date', '', _('use the given date as the sending date')),
471 ('', 'date', '', _('use the given date as the sending date')),
477 ('', 'desc', '', _('use the given file as the series description')),
472 ('', 'desc', '', _('use the given file as the series description')),
478 ('f', 'from', '', _('email address of sender')),
473 ('f', 'from', '', _('email address of sender')),
479 ('n', 'test', None, _('print messages that would be sent')),
474 ('n', 'test', None, _('print messages that would be sent')),
480 ('m', 'mbox', '',
475 ('m', 'mbox', '',
481 _('write messages to mbox file instead of sending them')),
476 _('write messages to mbox file instead of sending them')),
482 ('s', 'subject', '',
477 ('s', 'subject', '',
483 _('subject of first message (intro or single patch)')),
478 _('subject of first message (intro or single patch)')),
484 ('', 'in-reply-to', '',
479 ('', 'in-reply-to', '',
485 _('message identifier to reply to')),
480 _('message identifier to reply to')),
486 ('t', 'to', [], _('email addresses of recipients')),
481 ('t', 'to', [], _('email addresses of recipients')),
487 ]
482 ]
488
483
489
484
490 cmdtable = {
485 cmdtable = {
491 "email":
486 "email":
492 (patchbomb,
487 (patchbomb,
493 [('g', 'git', None, _('use git extended diff format')),
488 [('g', 'git', None, _('use git extended diff format')),
494 ('', 'plain', None, _('omit hg patch header')),
489 ('', 'plain', None, _('omit hg patch header')),
495 ('o', 'outgoing', None,
490 ('o', 'outgoing', None,
496 _('send changes not found in the target repository')),
491 _('send changes not found in the target repository')),
497 ('b', 'bundle', None,
492 ('b', 'bundle', None,
498 _('send changes not in target as a binary bundle')),
493 _('send changes not in target as a binary bundle')),
499 ('', 'bundlename', 'bundle',
494 ('', 'bundlename', 'bundle',
500 _('name of the bundle attachment file')),
495 _('name of the bundle attachment file')),
501 ('r', 'rev', [], _('a revision to send')),
496 ('r', 'rev', [], _('a revision to send')),
502 ('', 'force', None,
497 ('', 'force', None,
503 _('run even when remote repository is unrelated '
498 _('run even when remote repository is unrelated '
504 '(with -b/--bundle)')),
499 '(with -b/--bundle)')),
505 ('', 'base', [],
500 ('', 'base', [],
506 _('a base changeset to specify instead of a destination '
501 _('a base changeset to specify instead of a destination '
507 '(with -b/--bundle)')),
502 '(with -b/--bundle)')),
508 ('', 'intro', None,
503 ('', 'intro', None,
509 _('send an introduction email for a single patch')),
504 _('send an introduction email for a single patch')),
510 ] + emailopts + commands.remoteopts,
505 ] + emailopts + commands.remoteopts,
511 _('hg email [OPTION]... [DEST]...'))
506 _('hg email [OPTION]... [DEST]...'))
512 }
507 }
@@ -1,110 +1,106 b''
1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 #
2 #
3 # This is a small extension for Mercurial (http://www.selenic.com/mercurial)
3 # This is a small extension for Mercurial (http://www.selenic.com/mercurial)
4 # that removes files not known to mercurial
4 # that removes files not known to mercurial
5 #
5 #
6 # This program was inspired by the "cvspurge" script contained in CVS utilities
6 # This program was inspired by the "cvspurge" script contained in CVS utilities
7 # (http://www.red-bean.com/cvsutils/).
7 # (http://www.red-bean.com/cvsutils/).
8 #
8 #
9 # To enable the "purge" extension put these lines in your ~/.hgrc:
10 # [extensions]
11 # hgext.purge =
12 #
13 # For help on the usage of "hg purge" use:
9 # For help on the usage of "hg purge" use:
14 # hg help purge
10 # hg help purge
15 #
11 #
16 # This program is free software; you can redistribute it and/or modify
12 # This program is free software; you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
13 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation; either version 2 of the License, or
14 # the Free Software Foundation; either version 2 of the License, or
19 # (at your option) any later version.
15 # (at your option) any later version.
20 #
16 #
21 # This program is distributed in the hope that it will be useful,
17 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
20 # GNU General Public License for more details.
25 #
21 #
26 # You should have received a copy of the GNU General Public License
22 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
23 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29
25
30 from mercurial import util, commands, cmdutil
26 from mercurial import util, commands, cmdutil
31 from mercurial.i18n import _
27 from mercurial.i18n import _
32 import os, stat
28 import os, stat
33
29
34 def purge(ui, repo, *dirs, **opts):
30 def purge(ui, repo, *dirs, **opts):
35 '''removes files not tracked by Mercurial
31 '''removes files not tracked by Mercurial
36
32
37 Delete files not known to Mercurial. This is useful to test local
33 Delete files not known to Mercurial. This is useful to test local
38 and uncommitted changes in an otherwise-clean source tree.
34 and uncommitted changes in an otherwise-clean source tree.
39
35
40 This means that purge will delete:
36 This means that purge will delete:
41 - Unknown files: files marked with "?" by "hg status"
37 - Unknown files: files marked with "?" by "hg status"
42 - Empty directories: in fact Mercurial ignores directories unless
38 - Empty directories: in fact Mercurial ignores directories unless
43 they contain files under source control management
39 they contain files under source control management
44 But it will leave untouched:
40 But it will leave untouched:
45 - Modified and unmodified tracked files
41 - Modified and unmodified tracked files
46 - Ignored files (unless --all is specified)
42 - Ignored files (unless --all is specified)
47 - New files added to the repository (with "hg add")
43 - New files added to the repository (with "hg add")
48
44
49 If directories are given on the command line, only files in these
45 If directories are given on the command line, only files in these
50 directories are considered.
46 directories are considered.
51
47
52 Be careful with purge, as you could irreversibly delete some files
48 Be careful with purge, as you could irreversibly delete some files
53 you forgot to add to the repository. If you only want to print the
49 you forgot to add to the repository. If you only want to print the
54 list of files that this program would delete, use the --print
50 list of files that this program would delete, use the --print
55 option.
51 option.
56 '''
52 '''
57 act = not opts['print']
53 act = not opts['print']
58 eol = '\n'
54 eol = '\n'
59 if opts['print0']:
55 if opts['print0']:
60 eol = '\0'
56 eol = '\0'
61 act = False # --print0 implies --print
57 act = False # --print0 implies --print
62
58
63 def remove(remove_func, name):
59 def remove(remove_func, name):
64 if act:
60 if act:
65 try:
61 try:
66 remove_func(repo.wjoin(name))
62 remove_func(repo.wjoin(name))
67 except OSError:
63 except OSError:
68 m = _('%s cannot be removed') % name
64 m = _('%s cannot be removed') % name
69 if opts['abort_on_err']:
65 if opts['abort_on_err']:
70 raise util.Abort(m)
66 raise util.Abort(m)
71 ui.warn(_('warning: %s\n') % m)
67 ui.warn(_('warning: %s\n') % m)
72 else:
68 else:
73 ui.write('%s%s' % (name, eol))
69 ui.write('%s%s' % (name, eol))
74
70
75 def removefile(path):
71 def removefile(path):
76 try:
72 try:
77 os.remove(path)
73 os.remove(path)
78 except OSError:
74 except OSError:
79 # read-only files cannot be unlinked under Windows
75 # read-only files cannot be unlinked under Windows
80 s = os.stat(path)
76 s = os.stat(path)
81 if (s.st_mode & stat.S_IWRITE) != 0:
77 if (s.st_mode & stat.S_IWRITE) != 0:
82 raise
78 raise
83 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
79 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
84 os.remove(path)
80 os.remove(path)
85
81
86 directories = []
82 directories = []
87 match = cmdutil.match(repo, dirs, opts)
83 match = cmdutil.match(repo, dirs, opts)
88 match.dir = directories.append
84 match.dir = directories.append
89 status = repo.status(match=match, ignored=opts['all'], unknown=True)
85 status = repo.status(match=match, ignored=opts['all'], unknown=True)
90
86
91 for f in sorted(status[4] + status[5]):
87 for f in sorted(status[4] + status[5]):
92 ui.note(_('Removing file %s\n') % f)
88 ui.note(_('Removing file %s\n') % f)
93 remove(removefile, f)
89 remove(removefile, f)
94
90
95 for f in sorted(directories, reverse=True):
91 for f in sorted(directories, reverse=True):
96 if match(f) and not os.listdir(repo.wjoin(f)):
92 if match(f) and not os.listdir(repo.wjoin(f)):
97 ui.note(_('Removing directory %s\n') % f)
93 ui.note(_('Removing directory %s\n') % f)
98 remove(os.rmdir, f)
94 remove(os.rmdir, f)
99
95
100 cmdtable = {
96 cmdtable = {
101 'purge|clean':
97 'purge|clean':
102 (purge,
98 (purge,
103 [('a', 'abort-on-err', None, _('abort if an error occurs')),
99 [('a', 'abort-on-err', None, _('abort if an error occurs')),
104 ('', 'all', None, _('purge ignored files too')),
100 ('', 'all', None, _('purge ignored files too')),
105 ('p', 'print', None, _('print filenames instead of deleting them')),
101 ('p', 'print', None, _('print filenames instead of deleting them')),
106 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
102 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
107 ' (implies -p/--print)')),
103 ' (implies -p/--print)')),
108 ] + commands.walkopts,
104 ] + commands.walkopts,
109 _('hg purge [OPTION]... [DIR]...'))
105 _('hg purge [OPTION]... [DIR]...'))
110 }
106 }
@@ -1,126 +1,121 b''
1 # win32mbcs.py -- MBCS filename support for Mercurial
1 # win32mbcs.py -- MBCS filename support for Mercurial
2 #
2 #
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 #
4 #
5 # Version: 0.2
5 # Version: 0.2
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2, incorporated herein by reference.
9 # GNU General Public License version 2, incorporated herein by reference.
10 #
10 #
11
11
12 """allow to use MBCS path with problematic encoding.
12 """allow to use MBCS path with problematic encoding.
13
13
14 Some MBCS encodings are not good for some path operations (i.e.
14 Some MBCS encodings are not good for some path operations (i.e.
15 splitting path, case conversion, etc.) with its encoded bytes. We call
15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 This extension can be used to fix the issue with those encodings by
17 This extension can be used to fix the issue with those encodings by
18 wrapping some functions to convert to Unicode string before path
18 wrapping some functions to convert to Unicode string before path
19 operation.
19 operation.
20
20
21 This extension is useful for:
21 This extension is useful for:
22 * Japanese Windows users using shift_jis encoding.
22 * Japanese Windows users using shift_jis encoding.
23 * Chinese Windows users using big5 encoding.
23 * Chinese Windows users using big5 encoding.
24 * All users who use a repository with one of problematic encodings on
24 * All users who use a repository with one of problematic encodings on
25 case-insensitive file system.
25 case-insensitive file system.
26
26
27 This extension is not needed for:
27 This extension is not needed for:
28 * Any user who use only ASCII chars in path.
28 * Any user who use only ASCII chars in path.
29 * Any user who do not use any of problematic encodings.
29 * Any user who do not use any of problematic encodings.
30
30
31 Note that there are some limitations on using this extension:
31 Note that there are some limitations on using this extension:
32 * You should use single encoding in one repository.
32 * You should use single encoding in one repository.
33 * You should set same encoding for the repository by locale or
33 * You should set same encoding for the repository by locale or
34 HGENCODING.
34 HGENCODING.
35
35
36 To use this extension, enable the extension in .hg/hgrc or ~/.hgrc:
37
38 [extensions]
39 hgext.win32mbcs =
40
41 Path encoding conversion are done between Unicode and
36 Path encoding conversion are done between Unicode and
42 encoding.encoding which is decided by Mercurial from current locale
37 encoding.encoding which is decided by Mercurial from current locale
43 setting or HGENCODING.
38 setting or HGENCODING.
44
39
45 """
40 """
46
41
47 import os
42 import os
48 from mercurial.i18n import _
43 from mercurial.i18n import _
49 from mercurial import util, encoding
44 from mercurial import util, encoding
50
45
51 def decode(arg):
46 def decode(arg):
52 if isinstance(arg, str):
47 if isinstance(arg, str):
53 uarg = arg.decode(encoding.encoding)
48 uarg = arg.decode(encoding.encoding)
54 if arg == uarg.encode(encoding.encoding):
49 if arg == uarg.encode(encoding.encoding):
55 return uarg
50 return uarg
56 raise UnicodeError("Not local encoding")
51 raise UnicodeError("Not local encoding")
57 elif isinstance(arg, tuple):
52 elif isinstance(arg, tuple):
58 return tuple(map(decode, arg))
53 return tuple(map(decode, arg))
59 elif isinstance(arg, list):
54 elif isinstance(arg, list):
60 return map(decode, arg)
55 return map(decode, arg)
61 return arg
56 return arg
62
57
63 def encode(arg):
58 def encode(arg):
64 if isinstance(arg, unicode):
59 if isinstance(arg, unicode):
65 return arg.encode(encoding.encoding)
60 return arg.encode(encoding.encoding)
66 elif isinstance(arg, tuple):
61 elif isinstance(arg, tuple):
67 return tuple(map(encode, arg))
62 return tuple(map(encode, arg))
68 elif isinstance(arg, list):
63 elif isinstance(arg, list):
69 return map(encode, arg)
64 return map(encode, arg)
70 return arg
65 return arg
71
66
72 def wrapper(func, args):
67 def wrapper(func, args):
73 # check argument is unicode, then call original
68 # check argument is unicode, then call original
74 for arg in args:
69 for arg in args:
75 if isinstance(arg, unicode):
70 if isinstance(arg, unicode):
76 return func(*args)
71 return func(*args)
77
72
78 try:
73 try:
79 # convert arguments to unicode, call func, then convert back
74 # convert arguments to unicode, call func, then convert back
80 return encode(func(*decode(args)))
75 return encode(func(*decode(args)))
81 except UnicodeError:
76 except UnicodeError:
82 # If not encoded with encoding.encoding, report it then
77 # If not encoded with encoding.encoding, report it then
83 # continue with calling original function.
78 # continue with calling original function.
84 raise util.Abort(_("[win32mbcs] filename conversion fail with"
79 raise util.Abort(_("[win32mbcs] filename conversion fail with"
85 " %s encoding\n") % (encoding.encoding))
80 " %s encoding\n") % (encoding.encoding))
86
81
87 def wrapname(name):
82 def wrapname(name):
88 idx = name.rfind('.')
83 idx = name.rfind('.')
89 module = name[:idx]
84 module = name[:idx]
90 name = name[idx+1:]
85 name = name[idx+1:]
91 module = globals()[module]
86 module = globals()[module]
92 func = getattr(module, name)
87 func = getattr(module, name)
93 def f(*args):
88 def f(*args):
94 return wrapper(func, args)
89 return wrapper(func, args)
95 try:
90 try:
96 f.__name__ = func.__name__ # fail with python23
91 f.__name__ = func.__name__ # fail with python23
97 except Exception:
92 except Exception:
98 pass
93 pass
99 setattr(module, name, f)
94 setattr(module, name, f)
100
95
101 # List of functions to be wrapped.
96 # List of functions to be wrapped.
102 # NOTE: os.path.dirname() and os.path.basename() are safe because
97 # NOTE: os.path.dirname() and os.path.basename() are safe because
103 # they use result of os.path.split()
98 # they use result of os.path.split()
104 funcs = '''os.path.join os.path.split os.path.splitext
99 funcs = '''os.path.join os.path.split os.path.splitext
105 os.path.splitunc os.path.normpath os.path.normcase os.makedirs
100 os.path.splitunc os.path.normpath os.path.normcase os.makedirs
106 util.endswithsep util.splitpath util.checkcase util.fspath'''
101 util.endswithsep util.splitpath util.checkcase util.fspath'''
107
102
108 # codec and alias names of sjis and big5 to be faked.
103 # codec and alias names of sjis and big5 to be faked.
109 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
104 problematic_encodings = '''big5 big5-tw csbig5 big5hkscs big5-hkscs
110 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
105 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
111 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
106 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
112 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
107 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
113
108
114 def reposetup(ui, repo):
109 def reposetup(ui, repo):
115 # TODO: decide use of config section for this extension
110 # TODO: decide use of config section for this extension
116 if not os.path.supports_unicode_filenames:
111 if not os.path.supports_unicode_filenames:
117 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
112 ui.warn(_("[win32mbcs] cannot activate on this platform.\n"))
118 return
113 return
119
114
120 # fake is only for relevant environment.
115 # fake is only for relevant environment.
121 if encoding.encoding.lower() in problematic_encodings.split():
116 if encoding.encoding.lower() in problematic_encodings.split():
122 for f in funcs.split():
117 for f in funcs.split():
123 wrapname(f)
118 wrapname(f)
124 ui.debug(_("[win32mbcs] activated with encoding: %s\n")
119 ui.debug(_("[win32mbcs] activated with encoding: %s\n")
125 % encoding.encoding)
120 % encoding.encoding)
126
121
@@ -1,165 +1,159 b''
1 # zeroconf.py - zeroconf support for Mercurial
1 # zeroconf.py - zeroconf support for Mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''zeroconf support for Mercurial repositories
8 '''zeroconf support for Mercurial repositories
9
9
10 Zeroconf enabled repositories will be announced in a network without
10 Zeroconf enabled repositories will be announced in a network without
11 the need to configure a server or a service. They can be discovered
11 the need to configure a server or a service. They can be discovered
12 without knowing their actual IP address.
12 without knowing their actual IP address.
13
13
14 To use the zeroconf extension add the following entry to your hgrc
15 file:
16
17 [extensions]
18 hgext.zeroconf =
19
20 To allow other people to discover your repository using run "hg serve"
14 To allow other people to discover your repository using run "hg serve"
21 in your repository.
15 in your repository.
22
16
23 $ cd test
17 $ cd test
24 $ hg serve
18 $ hg serve
25
19
26 You can discover zeroconf enabled repositories by running "hg paths".
20 You can discover zeroconf enabled repositories by running "hg paths".
27
21
28 $ hg paths
22 $ hg paths
29 zc-test = http://example.com:8000/test
23 zc-test = http://example.com:8000/test
30 '''
24 '''
31
25
32 import Zeroconf, socket, time, os
26 import Zeroconf, socket, time, os
33 from mercurial import ui
27 from mercurial import ui
34 from mercurial import extensions
28 from mercurial import extensions
35 from mercurial.hgweb import hgweb_mod
29 from mercurial.hgweb import hgweb_mod
36 from mercurial.hgweb import hgwebdir_mod
30 from mercurial.hgweb import hgwebdir_mod
37
31
38 # publish
32 # publish
39
33
40 server = None
34 server = None
41 localip = None
35 localip = None
42
36
43 def getip():
37 def getip():
44 # finds external-facing interface without sending any packets (Linux)
38 # finds external-facing interface without sending any packets (Linux)
45 try:
39 try:
46 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
40 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
47 s.connect(('1.0.0.1', 0))
41 s.connect(('1.0.0.1', 0))
48 ip = s.getsockname()[0]
42 ip = s.getsockname()[0]
49 return ip
43 return ip
50 except:
44 except:
51 pass
45 pass
52
46
53 # Generic method, sometimes gives useless results
47 # Generic method, sometimes gives useless results
54 try:
48 try:
55 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
49 dumbip = socket.gethostbyaddr(socket.gethostname())[2][0]
56 if not dumbip.startswith('127.') and ':' not in dumbip:
50 if not dumbip.startswith('127.') and ':' not in dumbip:
57 return dumbip
51 return dumbip
58 except socket.gaierror:
52 except socket.gaierror:
59 dumbip = '127.0.0.1'
53 dumbip = '127.0.0.1'
60
54
61 # works elsewhere, but actually sends a packet
55 # works elsewhere, but actually sends a packet
62 try:
56 try:
63 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
57 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
64 s.connect(('1.0.0.1', 1))
58 s.connect(('1.0.0.1', 1))
65 ip = s.getsockname()[0]
59 ip = s.getsockname()[0]
66 return ip
60 return ip
67 except:
61 except:
68 pass
62 pass
69
63
70 return dumbip
64 return dumbip
71
65
72 def publish(name, desc, path, port):
66 def publish(name, desc, path, port):
73 global server, localip
67 global server, localip
74 if not server:
68 if not server:
75 ip = getip()
69 ip = getip()
76 if ip.startswith('127.'):
70 if ip.startswith('127.'):
77 # if we have no internet connection, this can happen.
71 # if we have no internet connection, this can happen.
78 return
72 return
79 localip = socket.inet_aton(ip)
73 localip = socket.inet_aton(ip)
80 server = Zeroconf.Zeroconf(ip)
74 server = Zeroconf.Zeroconf(ip)
81
75
82 hostname = socket.gethostname().split('.')[0]
76 hostname = socket.gethostname().split('.')[0]
83 host = hostname + ".local"
77 host = hostname + ".local"
84 name = "%s-%s" % (hostname, name)
78 name = "%s-%s" % (hostname, name)
85
79
86 # advertise to browsers
80 # advertise to browsers
87 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
81 svc = Zeroconf.ServiceInfo('_http._tcp.local.',
88 name + '._http._tcp.local.',
82 name + '._http._tcp.local.',
89 server = host,
83 server = host,
90 port = port,
84 port = port,
91 properties = {'description': desc,
85 properties = {'description': desc,
92 'path': "/" + path},
86 'path': "/" + path},
93 address = localip, weight = 0, priority = 0)
87 address = localip, weight = 0, priority = 0)
94 server.registerService(svc)
88 server.registerService(svc)
95
89
96 # advertise to Mercurial clients
90 # advertise to Mercurial clients
97 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
91 svc = Zeroconf.ServiceInfo('_hg._tcp.local.',
98 name + '._hg._tcp.local.',
92 name + '._hg._tcp.local.',
99 server = host,
93 server = host,
100 port = port,
94 port = port,
101 properties = {'description': desc,
95 properties = {'description': desc,
102 'path': "/" + path},
96 'path': "/" + path},
103 address = localip, weight = 0, priority = 0)
97 address = localip, weight = 0, priority = 0)
104 server.registerService(svc)
98 server.registerService(svc)
105
99
106 class hgwebzc(hgweb_mod.hgweb):
100 class hgwebzc(hgweb_mod.hgweb):
107 def __init__(self, repo, name=None):
101 def __init__(self, repo, name=None):
108 super(hgwebzc, self).__init__(repo, name)
102 super(hgwebzc, self).__init__(repo, name)
109 name = self.reponame or os.path.basename(repo.root)
103 name = self.reponame or os.path.basename(repo.root)
110 desc = self.repo.ui.config("web", "description", name)
104 desc = self.repo.ui.config("web", "description", name)
111 publish(name, desc, name, int(repo.ui.config("web", "port", 8000)))
105 publish(name, desc, name, int(repo.ui.config("web", "port", 8000)))
112
106
113 class hgwebdirzc(hgwebdir_mod.hgwebdir):
107 class hgwebdirzc(hgwebdir_mod.hgwebdir):
114 def run(self):
108 def run(self):
115 for r, p in self.repos:
109 for r, p in self.repos:
116 u = self.ui.copy()
110 u = self.ui.copy()
117 u.readconfig(os.path.join(p, '.hg', 'hgrc'))
111 u.readconfig(os.path.join(p, '.hg', 'hgrc'))
118 n = os.path.basename(r)
112 n = os.path.basename(r)
119 publish(n, "hgweb", p, int(u.config("web", "port", 8000)))
113 publish(n, "hgweb", p, int(u.config("web", "port", 8000)))
120 return super(hgwebdirzc, self).run()
114 return super(hgwebdirzc, self).run()
121
115
122 # listen
116 # listen
123
117
124 class listener(object):
118 class listener(object):
125 def __init__(self):
119 def __init__(self):
126 self.found = {}
120 self.found = {}
127 def removeService(self, server, type, name):
121 def removeService(self, server, type, name):
128 if repr(name) in self.found:
122 if repr(name) in self.found:
129 del self.found[repr(name)]
123 del self.found[repr(name)]
130 def addService(self, server, type, name):
124 def addService(self, server, type, name):
131 self.found[repr(name)] = server.getServiceInfo(type, name)
125 self.found[repr(name)] = server.getServiceInfo(type, name)
132
126
133 def getzcpaths():
127 def getzcpaths():
134 ip = getip()
128 ip = getip()
135 if ip.startswith('127.'):
129 if ip.startswith('127.'):
136 return
130 return
137 server = Zeroconf.Zeroconf(ip)
131 server = Zeroconf.Zeroconf(ip)
138 l = listener()
132 l = listener()
139 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
133 Zeroconf.ServiceBrowser(server, "_hg._tcp.local.", l)
140 time.sleep(1)
134 time.sleep(1)
141 server.close()
135 server.close()
142 for v in l.found.values():
136 for v in l.found.values():
143 n = v.name[:v.name.index('.')]
137 n = v.name[:v.name.index('.')]
144 n.replace(" ", "-")
138 n.replace(" ", "-")
145 u = "http://%s:%s%s" % (socket.inet_ntoa(v.address), v.port,
139 u = "http://%s:%s%s" % (socket.inet_ntoa(v.address), v.port,
146 v.properties.get("path", "/"))
140 v.properties.get("path", "/"))
147 yield "zc-" + n, u
141 yield "zc-" + n, u
148
142
149 def config(orig, self, section, key, default=None, untrusted=False):
143 def config(orig, self, section, key, default=None, untrusted=False):
150 if section == "paths" and key.startswith("zc-"):
144 if section == "paths" and key.startswith("zc-"):
151 for n, p in getzcpaths():
145 for n, p in getzcpaths():
152 if n == key:
146 if n == key:
153 return p
147 return p
154 return orig(self, section, key, default, untrusted)
148 return orig(self, section, key, default, untrusted)
155
149
156 def configitems(orig, self, section, untrusted=False):
150 def configitems(orig, self, section, untrusted=False):
157 r = orig(self, section, untrusted)
151 r = orig(self, section, untrusted)
158 if section == "paths":
152 if section == "paths":
159 r += getzcpaths()
153 r += getzcpaths()
160 return r
154 return r
161
155
162 extensions.wrapfunction(ui.ui, 'config', config)
156 extensions.wrapfunction(ui.ui, 'config', config)
163 extensions.wrapfunction(ui.ui, 'configitems', configitems)
157 extensions.wrapfunction(ui.ui, 'configitems', configitems)
164 hgweb_mod.hgweb = hgwebzc
158 hgweb_mod.hgweb = hgwebzc
165 hgwebdir_mod.hgwebdir = hgwebdirzc
159 hgwebdir_mod.hgwebdir = hgwebdirzc
General Comments 0
You need to be logged in to leave comments. Login now