##// END OF EJS Templates
convert comments to docstrings in a bunch of extensions
Dirkjan Ochtman -
r6666:53465a74 default
parent child Browse files
Show More
@@ -1,206 +1,205
1 1 # churn.py - create a graph showing who changed the most lines
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 #
8 #
9 # Aliases map file format is simple one alias per line in the following
10 # format:
11 #
12 # <alias email> <actual email>
7 '''allow graphing the number of lines changed per contributor'''
13 8
14 9 from mercurial.i18n import gettext as _
15 10 from mercurial import mdiff, cmdutil, util, node
16 11 import os, sys
17 12
18 13 def get_tty_width():
19 14 if 'COLUMNS' in os.environ:
20 15 try:
21 16 return int(os.environ['COLUMNS'])
22 17 except ValueError:
23 18 pass
24 19 try:
25 20 import termios, array, fcntl
26 21 for dev in (sys.stdout, sys.stdin):
27 22 try:
28 23 fd = dev.fileno()
29 24 if not os.isatty(fd):
30 25 continue
31 26 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
32 27 return array.array('h', arri)[1]
33 28 except ValueError:
34 29 pass
35 30 except ImportError:
36 31 pass
37 32 return 80
38 33
39 34 def __gather(ui, repo, node1, node2):
40 35 def dirtywork(f, mmap1, mmap2):
41 36 lines = 0
42 37
43 38 to = mmap1 and repo.file(f).read(mmap1[f]) or None
44 39 tn = mmap2 and repo.file(f).read(mmap2[f]) or None
45 40
46 41 diff = mdiff.unidiff(to, "", tn, "", f, f).split("\n")
47 42
48 43 for line in diff:
49 44 if not line:
50 45 continue # skip EOF
51 46 if line.startswith(" "):
52 47 continue # context line
53 48 if line.startswith("--- ") or line.startswith("+++ "):
54 49 continue # begining of diff
55 50 if line.startswith("@@ "):
56 51 continue # info line
57 52
58 53 # changed lines
59 54 lines += 1
60 55
61 56 return lines
62 57
63 58 ##
64 59
65 60 lines = 0
66 61
67 62 changes = repo.status(node1, node2)[:5]
68 63
69 64 modified, added, removed, deleted, unknown = changes
70 65
71 66 who = repo.changelog.read(node2)[1]
72 67 who = util.email(who) # get the email of the person
73 68
74 69 mmap1 = repo.manifest.read(repo.changelog.read(node1)[0])
75 70 mmap2 = repo.manifest.read(repo.changelog.read(node2)[0])
76 71 for f in modified:
77 72 lines += dirtywork(f, mmap1, mmap2)
78 73
79 74 for f in added:
80 75 lines += dirtywork(f, None, mmap2)
81 76
82 77 for f in removed:
83 78 lines += dirtywork(f, mmap1, None)
84 79
85 80 for f in deleted:
86 81 lines += dirtywork(f, mmap1, mmap2)
87 82
88 83 for f in unknown:
89 84 lines += dirtywork(f, mmap1, mmap2)
90 85
91 86 return (who, lines)
92 87
93 88 def gather_stats(ui, repo, amap, revs=None, progress=False):
94 89 stats = {}
95 90
96 91 cl = repo.changelog
97 92
98 93 if not revs:
99 94 revs = range(0, cl.count())
100 95
101 96 nr_revs = len(revs)
102 97 cur_rev = 0
103 98
104 99 for rev in revs:
105 100 cur_rev += 1 # next revision
106 101
107 102 node2 = cl.node(rev)
108 103 node1 = cl.parents(node2)[0]
109 104
110 105 if cl.parents(node2)[1] != node.nullid:
111 106 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
112 107 continue
113 108
114 109 who, lines = __gather(ui, repo, node1, node2)
115 110
116 111 # remap the owner if possible
117 112 if who in amap:
118 113 ui.note("using '%s' alias for '%s'\n" % (amap[who], who))
119 114 who = amap[who]
120 115
121 116 if not who in stats:
122 117 stats[who] = 0
123 118 stats[who] += lines
124 119
125 120 ui.note("rev %d: %d lines by %s\n" % (rev, lines, who))
126 121
127 122 if progress:
128 123 nr_revs = max(nr_revs, 1)
129 124 if int(100.0*(cur_rev - 1)/nr_revs) < int(100.0*cur_rev/nr_revs):
130 125 ui.write("\rGenerating stats: %d%%" % (int(100.0*cur_rev/nr_revs),))
131 126 sys.stdout.flush()
132 127
133 128 if progress:
134 129 ui.write("\r")
135 130 sys.stdout.flush()
136 131
137 132 return stats
138 133
139 134 def churn(ui, repo, **opts):
140 "Graphs the number of lines changed"
135 '''graphs the number of lines changed
136
137 The map file format used to specify aliases is fairly simple:
138
139 <alias email> <actual email>'''
141 140
142 141 def pad(s, l):
143 142 if len(s) < l:
144 143 return s + " " * (l-len(s))
145 144 return s[0:l]
146 145
147 146 def graph(n, maximum, width, char):
148 147 maximum = max(1, maximum)
149 148 n = int(n * width / float(maximum))
150 149
151 150 return char * (n)
152 151
153 152 def get_aliases(f):
154 153 aliases = {}
155 154
156 155 for l in f.readlines():
157 156 l = l.strip()
158 157 alias, actual = l.split()
159 158 aliases[alias] = actual
160 159
161 160 return aliases
162 161
163 162 amap = {}
164 163 aliases = opts.get('aliases')
165 164 if aliases:
166 165 try:
167 166 f = open(aliases,"r")
168 167 except OSError, e:
169 168 print "Error: " + e
170 169 return
171 170
172 171 amap = get_aliases(f)
173 172 f.close()
174 173
175 174 revs = [int(r) for r in cmdutil.revrange(repo, opts['rev'])]
176 175 revs.sort()
177 176 stats = gather_stats(ui, repo, amap, revs, opts.get('progress'))
178 177
179 178 # make a list of tuples (name, lines) and sort it in descending order
180 179 ordered = stats.items()
181 180 if not ordered:
182 181 return
183 182 ordered.sort(lambda x, y: cmp(y[1], x[1]))
184 183 max_churn = ordered[0][1]
185 184
186 185 tty_width = get_tty_width()
187 186 ui.note(_("assuming %i character terminal\n") % tty_width)
188 187 tty_width -= 1
189 188
190 189 max_user_width = max([len(user) for user, churn in ordered])
191 190
192 191 graph_width = tty_width - max_user_width - 1 - 6 - 2 - 2
193 192
194 193 for user, churn in ordered:
195 194 print "%s %6d %s" % (pad(user, max_user_width),
196 195 churn,
197 196 graph(churn, max_churn, graph_width, '*'))
198 197
199 198 cmdtable = {
200 199 "churn":
201 200 (churn,
202 201 [('r', 'rev', [], _('limit statistics to the specified revisions')),
203 202 ('', 'aliases', '', _('file with email aliases')),
204 203 ('', 'progress', None, _('show progress'))],
205 204 'hg churn [-r revision range] [-a file] [--progress]'),
206 205 }
@@ -1,149 +1,150
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 '''converting foreign VCS repositories to Mercurial'''
7 8
8 9 import convcmd
9 10 from mercurial import commands
10 11
11 12 # Commands definition was moved elsewhere to ease demandload job.
12 13
13 14 def convert(ui, src, dest=None, revmapfile=None, **opts):
14 15 """Convert a foreign SCM repository to a Mercurial one.
15 16
16 17 Accepted source formats:
17 18 - Mercurial
18 19 - CVS
19 20 - Darcs
20 21 - git
21 22 - Subversion
22 23 - Monotone
23 24 - GNU Arch
24 25
25 26 Accepted destination formats:
26 27 - Mercurial
27 28 - Subversion (history on branches is not preserved)
28 29
29 30 If no revision is given, all revisions will be converted. Otherwise,
30 31 convert will only import up to the named revision (given in a format
31 32 understood by the source).
32 33
33 34 If no destination directory name is specified, it defaults to the
34 35 basename of the source with '-hg' appended. If the destination
35 36 repository doesn't exist, it will be created.
36 37
37 38 If <REVMAP> isn't given, it will be put in a default location
38 39 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
39 40 file that maps each source commit ID to the destination ID for
40 41 that revision, like so:
41 42 <source ID> <destination ID>
42 43
43 44 If the file doesn't exist, it's automatically created. It's updated
44 45 on each commit copied, so convert-repo can be interrupted and can
45 46 be run repeatedly to copy new commits.
46 47
47 48 The [username mapping] file is a simple text file that maps each source
48 49 commit author to a destination commit author. It is handy for source SCMs
49 50 that use unix logins to identify authors (eg: CVS). One line per author
50 51 mapping and the line format is:
51 52 srcauthor=whatever string you want
52 53
53 54 The filemap is a file that allows filtering and remapping of files
54 55 and directories. Comment lines start with '#'. Each line can
55 56 contain one of the following directives:
56 57
57 58 include path/to/file
58 59
59 60 exclude path/to/file
60 61
61 62 rename from/file to/file
62 63
63 64 The 'include' directive causes a file, or all files under a
64 65 directory, to be included in the destination repository, and the
65 66 exclusion of all other files and dirs not explicitely included.
66 67 The 'exclude' directive causes files or directories to be omitted.
67 68 The 'rename' directive renames a file or directory. To rename from a
68 69 subdirectory into the root of the repository, use '.' as the path to
69 70 rename to.
70 71
71 72 The splicemap is a file that allows insertion of synthetic
72 73 history, letting you specify the parents of a revision. This is
73 74 useful if you want to e.g. give a Subversion merge two parents, or
74 75 graft two disconnected series of history together. Each entry
75 76 contains a key, followed by a space, followed by one or two
76 77 values, separated by spaces. The key is the revision ID in the
77 78 source revision control system whose parents should be modified
78 79 (same format as a key in .hg/shamap). The values are the revision
79 80 IDs (in either the source or destination revision control system)
80 81 that should be used as the new parents for that node.
81 82
82 83 Mercurial Source
83 84 -----------------
84 85
85 86 --config convert.hg.saverev=True (boolean)
86 87 allow target to preserve source revision ID
87 88
88 89 Subversion Source
89 90 -----------------
90 91
91 92 Subversion source detects classical trunk/branches/tags layouts.
92 93 By default, the supplied "svn://repo/path/" source URL is
93 94 converted as a single branch. If "svn://repo/path/trunk" exists
94 95 it replaces the default branch. If "svn://repo/path/branches"
95 96 exists, its subdirectories are listed as possible branches. If
96 97 "svn://repo/path/tags" exists, it is looked for tags referencing
97 98 converted branches. Default "trunk", "branches" and "tags" values
98 99 can be overriden with following options. Set them to paths
99 100 relative to the source URL, or leave them blank to disable
100 101 autodetection.
101 102
102 103 --config convert.svn.branches=branches (directory name)
103 104 specify the directory containing branches
104 105 --config convert.svn.tags=tags (directory name)
105 106 specify the directory containing tags
106 107 --config convert.svn.trunk=trunk (directory name)
107 108 specify the name of the trunk branch
108 109
109 110 Source history can be retrieved starting at a specific revision,
110 111 instead of being integrally converted. Only single branch
111 112 conversions are supported.
112 113
113 114 --config convert.svn.startrev=0 (svn revision number)
114 115 specify start Subversion revision.
115 116
116 117 Mercurial Destination
117 118 ---------------------
118 119
119 120 --config convert.hg.clonebranches=False (boolean)
120 121 dispatch source branches in separate clones.
121 122 --config convert.hg.tagsbranch=default (branch name)
122 123 tag revisions branch name
123 124 --config convert.hg.usebranchnames=True (boolean)
124 125 preserve branch names
125 126
126 127 """
127 128 return convcmd.convert(ui, src, dest, revmapfile, **opts)
128 129
129 130 def debugsvnlog(ui, **opts):
130 131 return convcmd.debugsvnlog(ui, **opts)
131 132
132 133 commands.norepo += " convert debugsvnlog"
133 134
134 135 cmdtable = {
135 136 "convert":
136 137 (convert,
137 138 [('A', 'authors', '', 'username mapping filename'),
138 139 ('d', 'dest-type', '', 'destination repository type'),
139 140 ('', 'filemap', '', 'remap file names using contents of file'),
140 141 ('r', 'rev', '', 'import up to target revision REV'),
141 142 ('s', 'source-type', '', 'source repository type'),
142 143 ('', 'splicemap', '', 'splice synthesized history into place'),
143 144 ('', 'datesort', None, 'try to sort changesets by date')],
144 145 'hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
145 146 "debugsvnlog":
146 147 (debugsvnlog,
147 148 [],
148 149 'hg debugsvnlog'),
149 150 }
@@ -1,127 +1,128
1 1 # fetch.py - pull and merge remote changes
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 '''pulling, updating and merging in one command'''
7 8
8 9 from mercurial.i18n import _
9 10 from mercurial.node import nullid, short
10 11 from mercurial import commands, cmdutil, hg, util
11 12
12 13 def fetch(ui, repo, source='default', **opts):
13 14 '''Pull changes from a remote repository, merge new changes if needed.
14 15
15 16 This finds all changes from the repository at the specified path
16 17 or URL and adds them to the local repository.
17 18
18 19 If the pulled changes add a new head, the head is automatically
19 20 merged, and the result of the merge is committed. Otherwise, the
20 21 working directory is updated to include the new changes.
21 22
22 23 When a merge occurs, the newly pulled changes are assumed to be
23 24 "authoritative". The head of the new changes is used as the first
24 25 parent, with local changes as the second. To switch the merge
25 26 order, use --switch-parent.
26 27
27 28 See 'hg help dates' for a list of formats valid for -d/--date.
28 29 '''
29 30
30 31 def postincoming(other, modheads):
31 32 if modheads == 0:
32 33 return 0
33 34 if modheads == 1:
34 35 return hg.clean(repo, repo.changelog.tip())
35 36 newheads = repo.heads(parent)
36 37 newchildren = [n for n in repo.heads(parent) if n != parent]
37 38 newparent = parent
38 39 if newchildren:
39 40 newparent = newchildren[0]
40 41 hg.clean(repo, newparent)
41 42 newheads = [n for n in repo.heads() if n != newparent]
42 43 if len(newheads) > 1:
43 44 ui.status(_('not merging with %d other new heads '
44 45 '(use "hg heads" and "hg merge" to merge them)') %
45 46 (len(newheads) - 1))
46 47 return
47 48 err = False
48 49 if newheads:
49 50 # By default, we consider the repository we're pulling
50 51 # *from* as authoritative, so we merge our changes into
51 52 # theirs.
52 53 if opts['switch_parent']:
53 54 firstparent, secondparent = newparent, newheads[0]
54 55 else:
55 56 firstparent, secondparent = newheads[0], newparent
56 57 ui.status(_('updating to %d:%s\n') %
57 58 (repo.changelog.rev(firstparent),
58 59 short(firstparent)))
59 60 hg.clean(repo, firstparent)
60 61 ui.status(_('merging with %d:%s\n') %
61 62 (repo.changelog.rev(secondparent), short(secondparent)))
62 63 err = hg.merge(repo, secondparent, remind=False)
63 64 if not err:
64 65 mod, add, rem = repo.status()[:3]
65 66 message = (cmdutil.logmessage(opts) or
66 67 (_('Automated merge with %s') %
67 68 util.removeauth(other.url())))
68 69 force_editor = opts.get('force_editor') or opts.get('edit')
69 70 n = repo.commit(mod + add + rem, message,
70 71 opts['user'], opts['date'], force=True,
71 72 force_editor=force_editor)
72 73 ui.status(_('new changeset %d:%s merges remote changes '
73 74 'with local\n') % (repo.changelog.rev(n),
74 75 short(n)))
75 76
76 77 def pull():
77 78 cmdutil.setremoteconfig(ui, opts)
78 79
79 80 other = hg.repository(ui, ui.expandpath(source))
80 81 ui.status(_('pulling from %s\n') %
81 82 util.hidepassword(ui.expandpath(source)))
82 83 revs = None
83 84 if opts['rev']:
84 85 if not other.local():
85 86 raise util.Abort(_("fetch -r doesn't work for remote "
86 87 "repositories yet"))
87 88 else:
88 89 revs = [other.lookup(rev) for rev in opts['rev']]
89 90 modheads = repo.pull(other, heads=revs)
90 91 return postincoming(other, modheads)
91 92
92 93 date = opts.get('date')
93 94 if date:
94 95 opts['date'] = util.parsedate(date)
95 96
96 97 parent, p2 = repo.dirstate.parents()
97 98 if parent != repo.changelog.tip():
98 99 raise util.Abort(_('working dir not at tip '
99 100 '(use "hg update" to check out tip)'))
100 101 if p2 != nullid:
101 102 raise util.Abort(_('outstanding uncommitted merge'))
102 103 wlock = lock = None
103 104 try:
104 105 wlock = repo.wlock()
105 106 lock = repo.lock()
106 107 mod, add, rem, del_ = repo.status()[:4]
107 108 if mod or add or rem:
108 109 raise util.Abort(_('outstanding uncommitted changes'))
109 110 if del_:
110 111 raise util.Abort(_('working directory is missing some files'))
111 112 if len(repo.heads()) > 1:
112 113 raise util.Abort(_('multiple heads in this repository '
113 114 '(use "hg heads" and "hg merge" to merge)'))
114 115 return pull()
115 116 finally:
116 117 del lock, wlock
117 118
118 119 cmdtable = {
119 120 'fetch':
120 121 (fetch,
121 122 [('r', 'rev', [], _('a specific revision you would like to pull')),
122 123 ('e', 'edit', None, _('edit commit message')),
123 124 ('', 'force-editor', None, _('edit commit message (DEPRECATED)')),
124 125 ('', 'switch-parent', None, _('switch parents when merging')),
125 126 ] + commands.commitopts + commands.commitopts2 + commands.remoteopts,
126 127 _('hg fetch [SOURCE]')),
127 128 }
@@ -1,326 +1,327
1 1 # ASCII graph log extension for Mercurial
2 2 #
3 3 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of
6 6 # the GNU General Public License, incorporated herein by reference.
7 '''show revision graphs in terminal windows'''
7 8
8 9 import os
9 10 import sys
10 11 from mercurial.cmdutil import revrange, show_changeset
11 12 from mercurial.commands import templateopts
12 13 from mercurial.i18n import _
13 14 from mercurial.node import nullrev
14 15 from mercurial.util import Abort, canonpath
15 16
16 17 def revision_grapher(repo, start_rev, stop_rev):
17 18 """incremental revision grapher
18 19
19 20 This generator function walks through the revision history from
20 21 revision start_rev to revision stop_rev (which must be less than
21 22 or equal to start_rev) and for each revision emits tuples with the
22 23 following elements:
23 24
24 25 - Current revision.
25 26 - Current node.
26 27 - Column of the current node in the set of ongoing edges.
27 28 - Edges; a list of (col, next_col) indicating the edges between
28 29 the current node and its parents.
29 30 - Number of columns (ongoing edges) in the current revision.
30 31 - The difference between the number of columns (ongoing edges)
31 32 in the next revision and the number of columns (ongoing edges)
32 33 in the current revision. That is: -1 means one column removed;
33 34 0 means no columns added or removed; 1 means one column added.
34 35 """
35 36
36 37 assert start_rev >= stop_rev
37 38 curr_rev = start_rev
38 39 revs = []
39 40 while curr_rev >= stop_rev:
40 41 node = repo.changelog.node(curr_rev)
41 42
42 43 # Compute revs and next_revs.
43 44 if curr_rev not in revs:
44 45 # New head.
45 46 revs.append(curr_rev)
46 47 rev_index = revs.index(curr_rev)
47 48 next_revs = revs[:]
48 49
49 50 # Add parents to next_revs.
50 51 parents = get_rev_parents(repo, curr_rev)
51 52 parents_to_add = []
52 53 for parent in parents:
53 54 if parent not in next_revs:
54 55 parents_to_add.append(parent)
55 56 parents_to_add.sort()
56 57 next_revs[rev_index:rev_index + 1] = parents_to_add
57 58
58 59 edges = []
59 60 for parent in parents:
60 61 edges.append((rev_index, next_revs.index(parent)))
61 62
62 63 n_columns_diff = len(next_revs) - len(revs)
63 64 yield (curr_rev, node, rev_index, edges, len(revs), n_columns_diff)
64 65
65 66 revs = next_revs
66 67 curr_rev -= 1
67 68
68 69 def filelog_grapher(repo, path, start_rev, stop_rev):
69 70 """incremental file log grapher
70 71
71 72 This generator function walks through the revision history of a
72 73 single file from revision start_rev to revision stop_rev (which must
73 74 be less than or equal to start_rev) and for each revision emits
74 75 tuples with the following elements:
75 76
76 77 - Current revision.
77 78 - Current node.
78 79 - Column of the current node in the set of ongoing edges.
79 80 - Edges; a list of (col, next_col) indicating the edges between
80 81 the current node and its parents.
81 82 - Number of columns (ongoing edges) in the current revision.
82 83 - The difference between the number of columns (ongoing edges)
83 84 in the next revision and the number of columns (ongoing edges)
84 85 in the current revision. That is: -1 means one column removed;
85 86 0 means no columns added or removed; 1 means one column added.
86 87 """
87 88
88 89 assert start_rev >= stop_rev
89 90 curr_rev = start_rev
90 91 revs = []
91 92 filerev = repo.file(path).count() - 1
92 93 while filerev >= 0:
93 94 fctx = repo.filectx(path, fileid=filerev)
94 95
95 96 # Compute revs and next_revs.
96 97 if filerev not in revs:
97 98 revs.append(filerev)
98 99 rev_index = revs.index(filerev)
99 100 next_revs = revs[:]
100 101
101 102 # Add parents to next_revs.
102 103 parents = [f.filerev() for f in fctx.parents() if f.path() == path]
103 104 parents_to_add = []
104 105 for parent in parents:
105 106 if parent not in next_revs:
106 107 parents_to_add.append(parent)
107 108 parents_to_add.sort()
108 109 next_revs[rev_index:rev_index + 1] = parents_to_add
109 110
110 111 edges = []
111 112 for parent in parents:
112 113 edges.append((rev_index, next_revs.index(parent)))
113 114
114 115 changerev = fctx.linkrev()
115 116 if changerev <= start_rev:
116 117 node = repo.changelog.node(changerev)
117 118 n_columns_diff = len(next_revs) - len(revs)
118 119 yield (changerev, node, rev_index, edges, len(revs), n_columns_diff)
119 120 if changerev <= stop_rev:
120 121 break
121 122 revs = next_revs
122 123 filerev -= 1
123 124
124 125 def get_rev_parents(repo, rev):
125 126 return [x for x in repo.changelog.parentrevs(rev) if x != nullrev]
126 127
127 128 def fix_long_right_edges(edges):
128 129 for (i, (start, end)) in enumerate(edges):
129 130 if end > start:
130 131 edges[i] = (start, end + 1)
131 132
132 133 def draw_edges(edges, nodeline, interline):
133 134 for (start, end) in edges:
134 135 if start == end + 1:
135 136 interline[2 * end + 1] = "/"
136 137 elif start == end - 1:
137 138 interline[2 * start + 1] = "\\"
138 139 elif start == end:
139 140 interline[2 * start] = "|"
140 141 else:
141 142 nodeline[2 * end] = "+"
142 143 if start > end:
143 144 (start, end) = (end,start)
144 145 for i in range(2 * start + 1, 2 * end):
145 146 if nodeline[i] != "+":
146 147 nodeline[i] = "-"
147 148
148 149 def format_line(line, level, logstr):
149 150 text = "%-*s %s" % (2 * level, "".join(line), logstr)
150 151 return "%s\n" % text.rstrip()
151 152
152 153 def get_nodeline_edges_tail(
153 154 node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
154 155 if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
155 156 # Still going in the same non-vertical direction.
156 157 if n_columns_diff == -1:
157 158 start = max(node_index + 1, p_node_index)
158 159 tail = ["|", " "] * (start - node_index - 1)
159 160 tail.extend(["/", " "] * (n_columns - start))
160 161 return tail
161 162 else:
162 163 return ["\\", " "] * (n_columns - node_index - 1)
163 164 else:
164 165 return ["|", " "] * (n_columns - node_index - 1)
165 166
166 167 def get_padding_line(ni, n_columns, edges):
167 168 line = []
168 169 line.extend(["|", " "] * ni)
169 170 if (ni, ni - 1) in edges or (ni, ni) in edges:
170 171 # (ni, ni - 1) (ni, ni)
171 172 # | | | | | | | |
172 173 # +---o | | o---+
173 174 # | | c | | c | |
174 175 # | |/ / | |/ /
175 176 # | | | | | |
176 177 c = "|"
177 178 else:
178 179 c = " "
179 180 line.extend([c, " "])
180 181 line.extend(["|", " "] * (n_columns - ni - 1))
181 182 return line
182 183
183 184 def get_limit(limit_opt):
184 185 if limit_opt:
185 186 try:
186 187 limit = int(limit_opt)
187 188 except ValueError:
188 189 raise Abort(_("limit must be a positive integer"))
189 190 if limit <= 0:
190 191 raise Abort(_("limit must be positive"))
191 192 else:
192 193 limit = sys.maxint
193 194 return limit
194 195
195 196 def get_revs(repo, rev_opt):
196 197 if rev_opt:
197 198 revs = revrange(repo, rev_opt)
198 199 return (max(revs), min(revs))
199 200 else:
200 201 return (repo.changelog.count() - 1, 0)
201 202
202 203 def graphlog(ui, repo, path=None, **opts):
203 204 """show revision history alongside an ASCII revision graph
204 205
205 206 Print a revision history alongside a revision graph drawn with
206 207 ASCII characters.
207 208
208 209 Nodes printed as an @ character are parents of the working
209 210 directory.
210 211 """
211 212
212 213 limit = get_limit(opts["limit"])
213 214 (start_rev, stop_rev) = get_revs(repo, opts["rev"])
214 215 stop_rev = max(stop_rev, start_rev - limit + 1)
215 216 if start_rev == nullrev:
216 217 return
217 218 cs_printer = show_changeset(ui, repo, opts)
218 219 if path:
219 220 cpath = canonpath(repo.root, os.getcwd(), path)
220 221 grapher = filelog_grapher(repo, cpath, start_rev, stop_rev)
221 222 else:
222 223 grapher = revision_grapher(repo, start_rev, stop_rev)
223 224 repo_parents = repo.dirstate.parents()
224 225 prev_n_columns_diff = 0
225 226 prev_node_index = 0
226 227
227 228 for (rev, node, node_index, edges, n_columns, n_columns_diff) in grapher:
228 229 # log_strings is the list of all log strings to draw alongside
229 230 # the graph.
230 231 ui.pushbuffer()
231 232 cs_printer.show(rev, node)
232 233 log_strings = ui.popbuffer().split("\n")[:-1]
233 234
234 235 if n_columns_diff == -1:
235 236 # Transform
236 237 #
237 238 # | | | | | |
238 239 # o | | into o---+
239 240 # |X / |/ /
240 241 # | | | |
241 242 fix_long_right_edges(edges)
242 243
243 244 # add_padding_line says whether to rewrite
244 245 #
245 246 # | | | | | | | |
246 247 # | o---+ into | o---+
247 248 # | / / | | | # <--- padding line
248 249 # o | | | / /
249 250 # o | |
250 251 add_padding_line = (len(log_strings) > 2 and
251 252 n_columns_diff == -1 and
252 253 [x for (x, y) in edges if x + 1 < y])
253 254
254 255 # fix_nodeline_tail says whether to rewrite
255 256 #
256 257 # | | o | | | | o | |
257 258 # | | |/ / | | |/ /
258 259 # | o | | into | o / / # <--- fixed nodeline tail
259 260 # | |/ / | |/ /
260 261 # o | | o | |
261 262 fix_nodeline_tail = len(log_strings) <= 2 and not add_padding_line
262 263
263 264 # nodeline is the line containing the node character (@ or o).
264 265 nodeline = ["|", " "] * node_index
265 266 if node in repo_parents:
266 267 node_ch = "@"
267 268 else:
268 269 node_ch = "o"
269 270 nodeline.extend([node_ch, " "])
270 271
271 272 nodeline.extend(
272 273 get_nodeline_edges_tail(
273 274 node_index, prev_node_index, n_columns, n_columns_diff,
274 275 prev_n_columns_diff, fix_nodeline_tail))
275 276
276 277 # shift_interline is the line containing the non-vertical
277 278 # edges between this entry and the next.
278 279 shift_interline = ["|", " "] * node_index
279 280 if n_columns_diff == -1:
280 281 n_spaces = 1
281 282 edge_ch = "/"
282 283 elif n_columns_diff == 0:
283 284 n_spaces = 2
284 285 edge_ch = "|"
285 286 else:
286 287 n_spaces = 3
287 288 edge_ch = "\\"
288 289 shift_interline.extend(n_spaces * [" "])
289 290 shift_interline.extend([edge_ch, " "] * (n_columns - node_index - 1))
290 291
291 292 # Draw edges from the current node to its parents.
292 293 draw_edges(edges, nodeline, shift_interline)
293 294
294 295 # lines is the list of all graph lines to print.
295 296 lines = [nodeline]
296 297 if add_padding_line:
297 298 lines.append(get_padding_line(node_index, n_columns, edges))
298 299 lines.append(shift_interline)
299 300
300 301 # Make sure that there are as many graph lines as there are
301 302 # log strings.
302 303 while len(log_strings) < len(lines):
303 304 log_strings.append("")
304 305 if len(lines) < len(log_strings):
305 306 extra_interline = ["|", " "] * (n_columns + n_columns_diff)
306 307 while len(lines) < len(log_strings):
307 308 lines.append(extra_interline)
308 309
309 310 # Print lines.
310 311 indentation_level = max(n_columns, n_columns + n_columns_diff)
311 312 for (line, logstr) in zip(lines, log_strings):
312 313 ui.write(format_line(line, indentation_level, logstr))
313 314
314 315 # ...and start over.
315 316 prev_node_index = node_index
316 317 prev_n_columns_diff = n_columns_diff
317 318
318 319 cmdtable = {
319 320 "glog":
320 321 (graphlog,
321 322 [('l', 'limit', '', _('limit number of changes displayed')),
322 323 ('p', 'patch', False, _('show patch')),
323 324 ('r', 'rev', [], _('show the specified revision or range')),
324 325 ] + templateopts,
325 326 _('hg glog [OPTION]... [FILE]')),
326 327 }
@@ -1,358 +1,357
1 1 # Minimal support for git commands on an hg repository
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 #
8 # The hgk extension allows browsing the history of a repository in a
9 # graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is
10 # not distributed with Mercurial.)
11 #
12 # hgk consists of two parts: a Tcl script that does the displaying and
13 # querying of information, and an extension to mercurial named hgk.py,
14 # which provides hooks for hgk to get information. hgk can be found in
15 # the contrib directory, and hgk.py can be found in the hgext
16 # directory.
17 #
18 # To load the hgext.py extension, add it to your .hgrc file (you have
19 # to use your global $HOME/.hgrc file, not one in a repository). You
20 # can specify an absolute path:
21 #
22 # [extensions]
23 # hgk=/usr/local/lib/hgk.py
24 #
25 # Mercurial can also scan the default python library path for a file
26 # named 'hgk.py' if you set hgk empty:
27 #
28 # [extensions]
29 # hgk=
30 #
31 # The hg view command will launch the hgk Tcl script. For this command
32 # to work, hgk must be in your search path. Alternately, you can
33 # specify the path to hgk in your .hgrc file:
34 #
35 # [hgk]
36 # path=/location/of/hgk
37 #
38 # hgk can make use of the extdiff extension to visualize
39 # revisions. Assuming you had already configured extdiff vdiff
40 # command, just add:
41 #
42 # [hgk]
43 # vdiff=vdiff
44 #
45 # Revisions context menu will now display additional entries to fire
46 # vdiff on hovered and selected revisions.
7 '''browsing the repository in a graphical way
8
9 The hgk extension allows browsing the history of a repository in a
10 graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is
11 not distributed with Mercurial.)
12
13 hgk consists of two parts: a Tcl script that does the displaying and
14 querying of information, and an extension to mercurial named hgk.py,
15 which provides hooks for hgk to get information. hgk can be found in
16 the contrib directory, and hgk.py can be found in the hgext directory.
17
18 To load the hgext.py extension, add it to your .hgrc file (you have
19 to use your global $HOME/.hgrc file, not one in a repository). You
20 can specify an absolute path:
21
22 [extensions]
23 hgk=/usr/local/lib/hgk.py
24
25 Mercurial can also scan the default python library path for a file
26 named 'hgk.py' if you set hgk empty:
27
28 [extensions]
29 hgk=
30
31 The hg view command will launch the hgk Tcl script. For this command
32 to work, hgk must be in your search path. Alternately, you can
33 specify the path to hgk in your .hgrc file:
34
35 [hgk]
36 path=/location/of/hgk
37
38 hgk can make use of the extdiff extension to visualize revisions.
39 Assuming you had already configured extdiff vdiff command, just add:
40
41 [hgk]
42 vdiff=vdiff
43
44 Revisions context menu will now display additional entries to fire
45 vdiff on hovered and selected revisions.'''
47 46
48 47 import os
49 48 from mercurial import commands, util, patch, revlog, cmdutil
50 49 from mercurial.node import nullid, nullrev, short
51 50
52 51 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
53 52 """diff trees from two commits"""
54 53 def __difftree(repo, node1, node2, files=[]):
55 54 assert node2 is not None
56 55 mmap = repo.changectx(node1).manifest()
57 56 mmap2 = repo.changectx(node2).manifest()
58 57 m = cmdutil.match(repo, files)
59 58 status = repo.status(node1, node2, match=m)[:5]
60 59 modified, added, removed, deleted, unknown = status
61 60
62 61 empty = short(nullid)
63 62
64 63 for f in modified:
65 64 # TODO get file permissions
66 65 ui.write(":100664 100664 %s %s M\t%s\t%s\n" %
67 66 (short(mmap[f]), short(mmap2[f]), f, f))
68 67 for f in added:
69 68 ui.write(":000000 100664 %s %s N\t%s\t%s\n" %
70 69 (empty, short(mmap2[f]), f, f))
71 70 for f in removed:
72 71 ui.write(":100664 000000 %s %s D\t%s\t%s\n" %
73 72 (short(mmap[f]), empty, f, f))
74 73 ##
75 74
76 75 while True:
77 76 if opts['stdin']:
78 77 try:
79 78 line = raw_input().split(' ')
80 79 node1 = line[0]
81 80 if len(line) > 1:
82 81 node2 = line[1]
83 82 else:
84 83 node2 = None
85 84 except EOFError:
86 85 break
87 86 node1 = repo.lookup(node1)
88 87 if node2:
89 88 node2 = repo.lookup(node2)
90 89 else:
91 90 node2 = node1
92 91 node1 = repo.changelog.parents(node1)[0]
93 92 if opts['patch']:
94 93 if opts['pretty']:
95 94 catcommit(ui, repo, node2, "")
96 95 m = cmdutil.match(repo, files)
97 96 patch.diff(repo, node1, node2, match=m,
98 97 opts=patch.diffopts(ui, {'git': True}))
99 98 else:
100 99 __difftree(repo, node1, node2, files=files)
101 100 if not opts['stdin']:
102 101 break
103 102
104 103 def catcommit(ui, repo, n, prefix, ctx=None):
105 104 nlprefix = '\n' + prefix;
106 105 if ctx is None:
107 106 ctx = repo.changectx(n)
108 107 (p1, p2) = ctx.parents()
109 108 ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ??
110 109 if p1: ui.write("parent %s\n" % short(p1.node()))
111 110 if p2: ui.write("parent %s\n" % short(p2.node()))
112 111 date = ctx.date()
113 112 description = ctx.description().replace("\0", "")
114 113 lines = description.splitlines()
115 114 if lines and lines[-1].startswith('committer:'):
116 115 committer = lines[-1].split(': ')[1].rstrip()
117 116 else:
118 117 committer = ctx.user()
119 118
120 119 ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))
121 120 ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1]))
122 121 ui.write("revision %d\n" % ctx.rev())
123 122 ui.write("branch %s\n\n" % ctx.branch())
124 123
125 124 if prefix != "":
126 125 ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip()))
127 126 else:
128 127 ui.write(description + "\n")
129 128 if prefix:
130 129 ui.write('\0')
131 130
132 131 def base(ui, repo, node1, node2):
133 132 """Output common ancestor information"""
134 133 node1 = repo.lookup(node1)
135 134 node2 = repo.lookup(node2)
136 135 n = repo.changelog.ancestor(node1, node2)
137 136 ui.write(short(n) + "\n")
138 137
139 138 def catfile(ui, repo, type=None, r=None, **opts):
140 139 """cat a specific revision"""
141 140 # in stdin mode, every line except the commit is prefixed with two
142 141 # spaces. This way the our caller can find the commit without magic
143 142 # strings
144 143 #
145 144 prefix = ""
146 145 if opts['stdin']:
147 146 try:
148 147 (type, r) = raw_input().split(' ');
149 148 prefix = " "
150 149 except EOFError:
151 150 return
152 151
153 152 else:
154 153 if not type or not r:
155 154 ui.warn("cat-file: type or revision not supplied\n")
156 155 commands.help_(ui, 'cat-file')
157 156
158 157 while r:
159 158 if type != "commit":
160 159 ui.warn("aborting hg cat-file only understands commits\n")
161 160 return 1;
162 161 n = repo.lookup(r)
163 162 catcommit(ui, repo, n, prefix)
164 163 if opts['stdin']:
165 164 try:
166 165 (type, r) = raw_input().split(' ');
167 166 except EOFError:
168 167 break
169 168 else:
170 169 break
171 170
172 171 # git rev-tree is a confusing thing. You can supply a number of
173 172 # commit sha1s on the command line, and it walks the commit history
174 173 # telling you which commits are reachable from the supplied ones via
175 174 # a bitmask based on arg position.
176 175 # you can specify a commit to stop at by starting the sha1 with ^
177 176 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
178 177 def chlogwalk():
179 178 count = repo.changelog.count()
180 179 i = count
181 180 l = [0] * 100
182 181 chunk = 100
183 182 while True:
184 183 if chunk > i:
185 184 chunk = i
186 185 i = 0
187 186 else:
188 187 i -= chunk
189 188
190 189 for x in xrange(0, chunk):
191 190 if i + x >= count:
192 191 l[chunk - x:] = [0] * (chunk - x)
193 192 break
194 193 if full != None:
195 194 l[x] = repo.changectx(i + x)
196 195 l[x].changeset() # force reading
197 196 else:
198 197 l[x] = 1
199 198 for x in xrange(chunk-1, -1, -1):
200 199 if l[x] != 0:
201 200 yield (i + x, full != None and l[x] or None)
202 201 if i == 0:
203 202 break
204 203
205 204 # calculate and return the reachability bitmask for sha
206 205 def is_reachable(ar, reachable, sha):
207 206 if len(ar) == 0:
208 207 return 1
209 208 mask = 0
210 209 for i in xrange(len(ar)):
211 210 if sha in reachable[i]:
212 211 mask |= 1 << i
213 212
214 213 return mask
215 214
216 215 reachable = []
217 216 stop_sha1 = []
218 217 want_sha1 = []
219 218 count = 0
220 219
221 220 # figure out which commits they are asking for and which ones they
222 221 # want us to stop on
223 222 for i in xrange(len(args)):
224 223 if args[i].startswith('^'):
225 224 s = repo.lookup(args[i][1:])
226 225 stop_sha1.append(s)
227 226 want_sha1.append(s)
228 227 elif args[i] != 'HEAD':
229 228 want_sha1.append(repo.lookup(args[i]))
230 229
231 230 # calculate the graph for the supplied commits
232 231 for i in xrange(len(want_sha1)):
233 232 reachable.append({});
234 233 n = want_sha1[i];
235 234 visit = [n];
236 235 reachable[i][n] = 1
237 236 while visit:
238 237 n = visit.pop(0)
239 238 if n in stop_sha1:
240 239 continue
241 240 for p in repo.changelog.parents(n):
242 241 if p not in reachable[i]:
243 242 reachable[i][p] = 1
244 243 visit.append(p)
245 244 if p in stop_sha1:
246 245 continue
247 246
248 247 # walk the repository looking for commits that are in our
249 248 # reachability graph
250 249 for i, ctx in chlogwalk():
251 250 n = repo.changelog.node(i)
252 251 mask = is_reachable(want_sha1, reachable, n)
253 252 if mask:
254 253 parentstr = ""
255 254 if parents:
256 255 pp = repo.changelog.parents(n)
257 256 if pp[0] != nullid:
258 257 parentstr += " " + short(pp[0])
259 258 if pp[1] != nullid:
260 259 parentstr += " " + short(pp[1])
261 260 if not full:
262 261 ui.write("%s%s\n" % (short(n), parentstr))
263 262 elif full == "commit":
264 263 ui.write("%s%s\n" % (short(n), parentstr))
265 264 catcommit(ui, repo, n, ' ', ctx)
266 265 else:
267 266 (p1, p2) = repo.changelog.parents(n)
268 267 (h, h1, h2) = map(short, (n, p1, p2))
269 268 (i1, i2) = map(repo.changelog.rev, (p1, p2))
270 269
271 270 date = ctx.date()[0]
272 271 ui.write("%s %s:%s" % (date, h, mask))
273 272 mask = is_reachable(want_sha1, reachable, p1)
274 273 if i1 != nullrev and mask > 0:
275 274 ui.write("%s:%s " % (h1, mask)),
276 275 mask = is_reachable(want_sha1, reachable, p2)
277 276 if i2 != nullrev and mask > 0:
278 277 ui.write("%s:%s " % (h2, mask))
279 278 ui.write("\n")
280 279 if maxnr and count >= maxnr:
281 280 break
282 281 count += 1
283 282
284 283 def revparse(ui, repo, *revs, **opts):
285 284 """Parse given revisions"""
286 285 def revstr(rev):
287 286 if rev == 'HEAD':
288 287 rev = 'tip'
289 288 return revlog.hex(repo.lookup(rev))
290 289
291 290 for r in revs:
292 291 revrange = r.split(':', 1)
293 292 ui.write('%s\n' % revstr(revrange[0]))
294 293 if len(revrange) == 2:
295 294 ui.write('^%s\n' % revstr(revrange[1]))
296 295
297 296 # git rev-list tries to order things by date, and has the ability to stop
298 297 # at a given commit without walking the whole repo. TODO add the stop
299 298 # parameter
300 299 def revlist(ui, repo, *revs, **opts):
301 300 """print revisions"""
302 301 if opts['header']:
303 302 full = "commit"
304 303 else:
305 304 full = None
306 305 copy = [x for x in revs]
307 306 revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
308 307
309 308 def config(ui, repo, **opts):
310 309 """print extension options"""
311 310 def writeopt(name, value):
312 311 ui.write('k=%s\nv=%s\n' % (name, value))
313 312
314 313 writeopt('vdiff', ui.config('hgk', 'vdiff', ''))
315 314
316 315
317 316 def view(ui, repo, *etc, **opts):
318 317 "start interactive history viewer"
319 318 os.chdir(repo.root)
320 319 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
321 320 cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc))
322 321 ui.debug("running %s\n" % cmd)
323 322 util.system(cmd)
324 323
325 324 cmdtable = {
326 325 "^view":
327 326 (view,
328 327 [('l', 'limit', '', 'limit number of changes displayed')],
329 328 'hg view [-l LIMIT] [REVRANGE]'),
330 329 "debug-diff-tree":
331 330 (difftree,
332 331 [('p', 'patch', None, 'generate patch'),
333 332 ('r', 'recursive', None, 'recursive'),
334 333 ('P', 'pretty', None, 'pretty'),
335 334 ('s', 'stdin', None, 'stdin'),
336 335 ('C', 'copy', None, 'detect copies'),
337 336 ('S', 'search', "", 'search')],
338 337 'hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...'),
339 338 "debug-cat-file":
340 339 (catfile,
341 340 [('s', 'stdin', None, 'stdin')],
342 341 'hg debug-cat-file [OPTION]... TYPE FILE'),
343 342 "debug-config":
344 343 (config, [], 'hg debug-config'),
345 344 "debug-merge-base":
346 345 (base, [], 'hg debug-merge-base node node'),
347 346 "debug-rev-parse":
348 347 (revparse,
349 348 [('', 'default', '', 'ignored')],
350 349 'hg debug-rev-parse REV'),
351 350 "debug-rev-list":
352 351 (revlist,
353 352 [('H', 'header', None, 'header'),
354 353 ('t', 'topo-order', None, 'topo-order'),
355 354 ('p', 'parents', None, 'parents'),
356 355 ('n', 'max-count', 0, 'max-count')],
357 356 'hg debug-rev-list [options] revs'),
358 357 }
@@ -1,100 +1,98
1 """
2 This is Mercurial extension for syntax highlighting in the file
3 revision view of hgweb.
1 """a mercurial extension for syntax highlighting in hgweb
4 2
5 3 It depends on the pygments syntax highlighting library:
6 4 http://pygments.org/
7 5
8 6 To enable the extension add this to hgrc:
9 7
10 8 [extensions]
11 9 hgext.highlight =
12 10
13 11 There is a single configuration option:
14 12
15 13 [web]
16 14 pygments_style = <style>
17 15
18 16 The default is 'colorful'.
19 17
20 18 -- Adam Hupp <adam@hupp.org>
21 19 """
22 20
23 21 from mercurial import demandimport
24 22 demandimport.ignore.extend(['pkgutil', 'pkg_resources', '__main__',])
25 23
26 24 from mercurial.hgweb import webcommands, webutil, common
27 25 from mercurial import util
28 26 from mercurial.templatefilters import filters
29 27
30 28 from pygments import highlight
31 29 from pygments.util import ClassNotFound
32 30 from pygments.lexers import guess_lexer, guess_lexer_for_filename, TextLexer
33 31 from pygments.formatters import HtmlFormatter
34 32
35 33 SYNTAX_CSS = ('\n<link rel="stylesheet" href="{url}highlightcss" '
36 34 'type="text/css" />')
37 35
38 36 def pygmentize(field, fctx, style, tmpl):
39 37
40 38 # append a <link ...> to the syntax highlighting css
41 39 old_header = ''.join(tmpl('header'))
42 40 if SYNTAX_CSS not in old_header:
43 41 new_header = old_header + SYNTAX_CSS
44 42 tmpl.cache['header'] = new_header
45 43
46 44 text = fctx.data()
47 45 if util.binary(text):
48 46 return
49 47
50 48 # To get multi-line strings right, we can't format line-by-line
51 49 try:
52 50 lexer = guess_lexer_for_filename(fctx.path(), text[:1024],
53 51 encoding=util._encoding)
54 52 except (ClassNotFound, ValueError):
55 53 try:
56 54 lexer = guess_lexer(text[:1024], encoding=util._encoding)
57 55 except (ClassNotFound, ValueError):
58 56 lexer = TextLexer(encoding=util._encoding)
59 57
60 58 formatter = HtmlFormatter(style=style, encoding=util._encoding)
61 59
62 60 colorized = highlight(text, lexer, formatter)
63 61 # strip wrapping div
64 62 colorized = colorized[:colorized.find('\n</pre>')]
65 63 colorized = colorized[colorized.find('<pre>')+5:]
66 64 coloriter = iter(colorized.splitlines())
67 65
68 66 filters['colorize'] = lambda x: coloriter.next()
69 67
70 68 oldl = tmpl.cache[field]
71 69 newl = oldl.replace('line|escape', 'line|colorize')
72 70 tmpl.cache[field] = newl
73 71
74 72 web_filerevision = webcommands._filerevision
75 73 web_annotate = webcommands.annotate
76 74
77 75 def filerevision_highlight(web, tmpl, fctx):
78 76 style = web.config('web', 'pygments_style', 'colorful')
79 77 pygmentize('fileline', fctx, style, tmpl)
80 78 return web_filerevision(web, tmpl, fctx)
81 79
82 80 def annotate_highlight(web, req, tmpl):
83 81 fctx = webutil.filectx(web.repo, req)
84 82 style = web.config('web', 'pygments_style', 'colorful')
85 83 pygmentize('annotateline', fctx, style, tmpl)
86 84 return web_annotate(web, req, tmpl)
87 85
88 86 def generate_css(web, req, tmpl):
89 87 pg_style = web.config('web', 'pygments_style', 'colorful')
90 88 fmter = HtmlFormatter(style = pg_style)
91 89 req.respond(common.HTTP_OK, 'text/css')
92 90 return ['/* pygments_style = %s */\n\n' % pg_style, fmter.get_style_defs('')]
93 91
94 92
95 93 # monkeypatch in the new version
96 94
97 95 webcommands._filerevision = filerevision_highlight
98 96 webcommands.annotate = annotate_highlight
99 97 webcommands.highlightcss = generate_css
100 98 webcommands.__all__.append('highlightcss')
@@ -1,469 +1,466
1 # Command for sending a collection of Mercurial changesets as a series
2 # of patch emails.
3 #
4 # The series is started off with a "[PATCH 0 of N]" introduction,
5 # which describes the series as a whole.
6 #
7 # Each patch email has a Subject line of "[PATCH M of N] ...", using
8 # the first line of the changeset description as the subject text.
9 # The message contains two or three body parts:
10 #
11 # The remainder of the changeset description.
12 #
13 # [Optional] If the diffstat program is installed, the result of
14 # running diffstat on the patch.
15 #
16 # The patch itself, as generated by "hg export".
17 #
18 # Each message refers to all of its predecessors using the In-Reply-To
19 # and References headers, so they will show up as a sequence in
20 # threaded mail and news readers, and in mail archives.
21 #
22 # For each changeset, you will be prompted with a diffstat summary and
23 # the changeset summary, so you can be sure you are sending the right
24 # changes.
25 #
26 # To enable this extension:
27 #
28 # [extensions]
29 # hgext.patchbomb =
30 #
31 # To configure other defaults, add a section like this to your hgrc
32 # file:
33 #
34 # [email]
35 # from = My Name <my@email>
36 # to = recipient1, recipient2, ...
37 # cc = cc1, cc2, ...
38 # bcc = bcc1, bcc2, ...
39 #
40 # Then you can use the "hg email" command to mail a series of changesets
41 # as a patchbomb.
42 #
43 # To avoid sending patches prematurely, it is a good idea to first run
44 # the "email" command with the "-n" option (test only). You will be
45 # prompted for an email recipient address, a subject an an introductory
46 # message describing the patches of your patchbomb. Then when all is
47 # done, patchbomb messages are displayed. If PAGER environment variable
48 # is set, your pager will be fired up once for each patchbomb message, so
49 # you can verify everything is alright.
50 #
51 # The "-m" (mbox) option is also very useful. Instead of previewing
52 # each patchbomb message in a pager or sending the messages directly,
53 # it will create a UNIX mailbox file with the patch emails. This
54 # mailbox file can be previewed with any mail user agent which supports
55 # UNIX mbox files, i.e. with mutt:
56 #
57 # % mutt -R -f mbox
58 #
59 # When you are previewing the patchbomb messages, you can use `formail'
60 # (a utility that is commonly installed as part of the procmail package),
61 # to send each message out:
62 #
63 # % formail -s sendmail -bm -t < mbox
64 #
65 # That should be all. Now your patchbomb is on its way out.
1 '''sending Mercurial changesets as a series of patch emails
2
3 The series is started off with a "[PATCH 0 of N]" introduction,
4 which describes the series as a whole.
5
6 Each patch email has a Subject line of "[PATCH M of N] ...", using
7 the first line of the changeset description as the subject text.
8 The message contains two or three body parts:
9
10 The remainder of the changeset description.
11
12 [Optional] If the diffstat program is installed, the result of
13 running diffstat on the patch.
14
15 The patch itself, as generated by "hg export".
16
17 Each message refers to all of its predecessors using the In-Reply-To
18 and References headers, so they will show up as a sequence in
19 threaded mail and news readers, and in mail archives.
20
21 For each changeset, you will be prompted with a diffstat summary and
22 the changeset summary, so you can be sure you are sending the right changes.
23
24 To enable this extension:
25
26 [extensions]
27 hgext.patchbomb =
28
29 To configure other defaults, add a section like this to your hgrc file:
30
31 [email]
32 from = My Name <my@email>
33 to = recipient1, recipient2, ...
34 cc = cc1, cc2, ...
35 bcc = bcc1, bcc2, ...
36
37 Then you can use the "hg email" command to mail a series of changesets
38 as a patchbomb.
39
40 To avoid sending patches prematurely, it is a good idea to first run
41 the "email" command with the "-n" option (test only). You will be
42 prompted for an email recipient address, a subject an an introductory
43 message describing the patches of your patchbomb. Then when all is
44 done, patchbomb messages are displayed. If PAGER environment variable
45 is set, your pager will be fired up once for each patchbomb message, so
46 you can verify everything is alright.
47
48 The "-m" (mbox) option is also very useful. Instead of previewing
49 each patchbomb message in a pager or sending the messages directly,
50 it will create a UNIX mailbox file with the patch emails. This
51 mailbox file can be previewed with any mail user agent which supports
52 UNIX mbox files, i.e. with mutt:
53
54 % mutt -R -f mbox
55
56 When you are previewing the patchbomb messages, you can use `formail'
57 (a utility that is commonly installed as part of the procmail package),
58 to send each message out:
59
60 % formail -s sendmail -bm -t < mbox
61
62 That should be all. Now your patchbomb is on its way out.'''
66 63
67 64 import os, errno, socket, tempfile, cStringIO
68 65 import email.MIMEMultipart, email.MIMEText, email.MIMEBase
69 66 import email.Utils, email.Encoders, email.Generator
70 67 from mercurial import cmdutil, commands, hg, mail, patch, util
71 68 from mercurial.i18n import _
72 69 from mercurial.node import bin
73 70
74 71 def patchbomb(ui, repo, *revs, **opts):
75 72 '''send changesets by email
76 73
77 74 By default, diffs are sent in the format generated by hg export,
78 75 one per message. The series starts with a "[PATCH 0 of N]"
79 76 introduction, which describes the series as a whole.
80 77
81 78 Each patch email has a Subject line of "[PATCH M of N] ...", using
82 79 the first line of the changeset description as the subject text.
83 80 The message contains two or three body parts. First, the rest of
84 81 the changeset description. Next, (optionally) if the diffstat
85 82 program is installed, the result of running diffstat on the patch.
86 83 Finally, the patch itself, as generated by "hg export".
87 84
88 85 With --outgoing, emails will be generated for patches not
89 86 found in the destination repository (or only those which are
90 87 ancestors of the specified revisions if any are provided)
91 88
92 89 With --bundle, changesets are selected as for --outgoing,
93 90 but a single email containing a binary Mercurial bundle as an
94 91 attachment will be sent.
95 92
96 93 Examples:
97 94
98 95 hg email -r 3000 # send patch 3000 only
99 96 hg email -r 3000 -r 3001 # send patches 3000 and 3001
100 97 hg email -r 3000:3005 # send patches 3000 through 3005
101 98 hg email 3000 # send patch 3000 (deprecated)
102 99
103 100 hg email -o # send all patches not in default
104 101 hg email -o DEST # send all patches not in DEST
105 102 hg email -o -r 3000 # send all ancestors of 3000 not in default
106 103 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
107 104
108 105 hg email -b # send bundle of all patches not in default
109 106 hg email -b DEST # send bundle of all patches not in DEST
110 107 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
111 108 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
112 109
113 110 Before using this command, you will need to enable email in your hgrc.
114 111 See the [email] section in hgrc(5) for details.
115 112 '''
116 113
117 114 def prompt(prompt, default = None, rest = ': ', empty_ok = False):
118 115 if not ui.interactive:
119 116 return default
120 117 if default:
121 118 prompt += ' [%s]' % default
122 119 prompt += rest
123 120 while True:
124 121 r = ui.prompt(prompt, default=default)
125 122 if r:
126 123 return r
127 124 if default is not None:
128 125 return default
129 126 if empty_ok:
130 127 return r
131 128 ui.warn(_('Please enter a valid value.\n'))
132 129
133 130 def confirm(s, denial):
134 131 if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
135 132 raise util.Abort(denial)
136 133
137 134 def cdiffstat(summary, patchlines):
138 135 s = patch.diffstat(patchlines)
139 136 if s:
140 137 if summary:
141 138 ui.write(summary, '\n')
142 139 ui.write(s, '\n')
143 140 confirm(_('Does the diffstat above look okay'),
144 141 _('diffstat rejected'))
145 142 elif s is None:
146 143 ui.warn(_('No diffstat information available.\n'))
147 144 s = ''
148 145 return s
149 146
150 147 def makepatch(patch, idx, total):
151 148 desc = []
152 149 node = None
153 150 body = ''
154 151 for line in patch:
155 152 if line.startswith('#'):
156 153 if line.startswith('# Node ID'):
157 154 node = line.split()[-1]
158 155 continue
159 156 if line.startswith('diff -r') or line.startswith('diff --git'):
160 157 break
161 158 desc.append(line)
162 159 if not node:
163 160 raise ValueError
164 161
165 162 if opts['attach']:
166 163 body = ('\n'.join(desc[1:]).strip() or
167 164 'Patch subject is complete summary.')
168 165 body += '\n\n\n'
169 166
170 167 if opts.get('plain'):
171 168 while patch and patch[0].startswith('# '):
172 169 patch.pop(0)
173 170 if patch:
174 171 patch.pop(0)
175 172 while patch and not patch[0].strip():
176 173 patch.pop(0)
177 174 if opts.get('diffstat'):
178 175 body += cdiffstat('\n'.join(desc), patch) + '\n\n'
179 176 if opts.get('attach') or opts.get('inline'):
180 177 msg = email.MIMEMultipart.MIMEMultipart()
181 178 if body:
182 179 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
183 180 p = email.MIMEText.MIMEText('\n'.join(patch), 'x-patch')
184 181 binnode = bin(node)
185 182 # if node is mq patch, it will have patch file name as tag
186 183 patchname = [t for t in repo.nodetags(binnode)
187 184 if t.endswith('.patch') or t.endswith('.diff')]
188 185 if patchname:
189 186 patchname = patchname[0]
190 187 elif total > 1:
191 188 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
192 189 binnode, idx, total)
193 190 else:
194 191 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
195 192 disposition = 'inline'
196 193 if opts['attach']:
197 194 disposition = 'attachment'
198 195 p['Content-Disposition'] = disposition + '; filename=' + patchname
199 196 msg.attach(p)
200 197 else:
201 198 body += '\n'.join(patch)
202 199 msg = email.MIMEText.MIMEText(body)
203 200
204 201 subj = desc[0].strip().rstrip('. ')
205 202 if total == 1:
206 203 subj = '[PATCH] ' + (opts.get('subject') or subj)
207 204 else:
208 205 tlen = len(str(total))
209 206 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
210 207 msg['Subject'] = subj
211 208 msg['X-Mercurial-Node'] = node
212 209 return msg
213 210
214 211 def outgoing(dest, revs):
215 212 '''Return the revisions present locally but not in dest'''
216 213 dest = ui.expandpath(dest or 'default-push', dest or 'default')
217 214 revs = [repo.lookup(rev) for rev in revs]
218 215 other = hg.repository(ui, dest)
219 216 ui.status(_('comparing with %s\n') % dest)
220 217 o = repo.findoutgoing(other)
221 218 if not o:
222 219 ui.status(_("no changes found\n"))
223 220 return []
224 221 o = repo.changelog.nodesbetween(o, revs or None)[0]
225 222 return [str(repo.changelog.rev(r)) for r in o]
226 223
227 224 def getbundle(dest):
228 225 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
229 226 tmpfn = os.path.join(tmpdir, 'bundle')
230 227 try:
231 228 commands.bundle(ui, repo, tmpfn, dest, **opts)
232 229 return open(tmpfn, 'rb').read()
233 230 finally:
234 231 try:
235 232 os.unlink(tmpfn)
236 233 except:
237 234 pass
238 235 os.rmdir(tmpdir)
239 236
240 237 if not (opts.get('test') or opts.get('mbox')):
241 238 # really sending
242 239 mail.validateconfig(ui)
243 240
244 241 if not (revs or opts.get('rev')
245 242 or opts.get('outgoing') or opts.get('bundle')):
246 243 raise util.Abort(_('specify at least one changeset with -r or -o'))
247 244
248 245 cmdutil.setremoteconfig(ui, opts)
249 246 if opts.get('outgoing') and opts.get('bundle'):
250 247 raise util.Abort(_("--outgoing mode always on with --bundle;"
251 248 " do not re-specify --outgoing"))
252 249
253 250 if opts.get('outgoing') or opts.get('bundle'):
254 251 if len(revs) > 1:
255 252 raise util.Abort(_("too many destinations"))
256 253 dest = revs and revs[0] or None
257 254 revs = []
258 255
259 256 if opts.get('rev'):
260 257 if revs:
261 258 raise util.Abort(_('use only one form to specify the revision'))
262 259 revs = opts.get('rev')
263 260
264 261 if opts.get('outgoing'):
265 262 revs = outgoing(dest, opts.get('rev'))
266 263 if opts.get('bundle'):
267 264 opts['revs'] = revs
268 265
269 266 # start
270 267 if opts.get('date'):
271 268 start_time = util.parsedate(opts.get('date'))
272 269 else:
273 270 start_time = util.makedate()
274 271
275 272 def genmsgid(id):
276 273 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
277 274
278 275 def getdescription(body, sender):
279 276 if opts.get('desc'):
280 277 body = open(opts.get('desc')).read()
281 278 else:
282 279 ui.write(_('\nWrite the introductory message for the '
283 280 'patch series.\n\n'))
284 281 body = ui.edit(body, sender)
285 282 return body
286 283
287 284 def getexportmsgs():
288 285 patches = []
289 286
290 287 class exportee:
291 288 def __init__(self, container):
292 289 self.lines = []
293 290 self.container = container
294 291 self.name = 'email'
295 292
296 293 def write(self, data):
297 294 self.lines.append(data)
298 295
299 296 def close(self):
300 297 self.container.append(''.join(self.lines).split('\n'))
301 298 self.lines = []
302 299
303 300 commands.export(ui, repo, *revs, **{'output': exportee(patches),
304 301 'switch_parent': False,
305 302 'text': None,
306 303 'git': opts.get('git')})
307 304
308 305 jumbo = []
309 306 msgs = []
310 307
311 308 ui.write(_('This patch series consists of %d patches.\n\n')
312 309 % len(patches))
313 310
314 311 for p, i in zip(patches, xrange(len(patches))):
315 312 jumbo.extend(p)
316 313 msgs.append(makepatch(p, i + 1, len(patches)))
317 314
318 315 if len(patches) > 1:
319 316 tlen = len(str(len(patches)))
320 317
321 318 subj = '[PATCH %0*d of %d] %s' % (
322 319 tlen, 0, len(patches),
323 320 opts.get('subject') or
324 321 prompt('Subject:',
325 322 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
326 323
327 324 body = ''
328 325 if opts.get('diffstat'):
329 326 d = cdiffstat(_('Final summary:\n'), jumbo)
330 327 if d:
331 328 body = '\n' + d
332 329
333 330 body = getdescription(body, sender)
334 331 msg = email.MIMEText.MIMEText(body)
335 332 msg['Subject'] = subj
336 333
337 334 msgs.insert(0, msg)
338 335 return msgs
339 336
340 337 def getbundlemsgs(bundle):
341 338 subj = (opts.get('subject')
342 339 or prompt('Subject:', default='A bundle for your repository'))
343 340
344 341 body = getdescription('', sender)
345 342 msg = email.MIMEMultipart.MIMEMultipart()
346 343 if body:
347 344 msg.attach(email.MIMEText.MIMEText(body, 'plain'))
348 345 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
349 346 datapart.set_payload(bundle)
350 347 datapart.add_header('Content-Disposition', 'attachment',
351 348 filename='bundle.hg')
352 349 email.Encoders.encode_base64(datapart)
353 350 msg.attach(datapart)
354 351 msg['Subject'] = subj
355 352 return [msg]
356 353
357 354 sender = (opts.get('from') or ui.config('email', 'from') or
358 355 ui.config('patchbomb', 'from') or
359 356 prompt('From', ui.username()))
360 357
361 358 if opts.get('bundle'):
362 359 msgs = getbundlemsgs(getbundle(dest))
363 360 else:
364 361 msgs = getexportmsgs()
365 362
366 363 def getaddrs(opt, prpt, default = None):
367 364 addrs = opts.get(opt) or (ui.config('email', opt) or
368 365 ui.config('patchbomb', opt) or
369 366 prompt(prpt, default = default)).split(',')
370 367 return [a.strip() for a in addrs if a.strip()]
371 368
372 369 to = getaddrs('to', 'To')
373 370 cc = getaddrs('cc', 'Cc', '')
374 371
375 372 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
376 373 ui.config('patchbomb', 'bcc') or '').split(',')
377 374 bcc = [a.strip() for a in bcc if a.strip()]
378 375
379 376 ui.write('\n')
380 377
381 378 parent = None
382 379
383 380 sender_addr = email.Utils.parseaddr(sender)[1]
384 381 sendmail = None
385 382 for m in msgs:
386 383 try:
387 384 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
388 385 except TypeError:
389 386 m['Message-Id'] = genmsgid('patchbomb')
390 387 if parent:
391 388 m['In-Reply-To'] = parent
392 389 else:
393 390 parent = m['Message-Id']
394 391 m['Date'] = util.datestr(start_time, "%a, %d %b %Y %H:%M:%S %1%2")
395 392
396 393 start_time = (start_time[0] + 1, start_time[1])
397 394 m['From'] = sender
398 395 m['To'] = ', '.join(to)
399 396 if cc:
400 397 m['Cc'] = ', '.join(cc)
401 398 if bcc:
402 399 m['Bcc'] = ', '.join(bcc)
403 400 if opts.get('test'):
404 401 ui.status('Displaying ', m['Subject'], ' ...\n')
405 402 ui.flush()
406 403 if 'PAGER' in os.environ:
407 404 fp = util.popen(os.environ['PAGER'], 'w')
408 405 else:
409 406 fp = ui
410 407 generator = email.Generator.Generator(fp, mangle_from_=False)
411 408 try:
412 409 generator.flatten(m, 0)
413 410 fp.write('\n')
414 411 except IOError, inst:
415 412 if inst.errno != errno.EPIPE:
416 413 raise
417 414 if fp is not ui:
418 415 fp.close()
419 416 elif opts.get('mbox'):
420 417 ui.status('Writing ', m['Subject'], ' ...\n')
421 418 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
422 419 generator = email.Generator.Generator(fp, mangle_from_=True)
423 420 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
424 421 fp.write('From %s %s\n' % (sender_addr, date))
425 422 generator.flatten(m, 0)
426 423 fp.write('\n\n')
427 424 fp.close()
428 425 else:
429 426 if not sendmail:
430 427 sendmail = mail.connect(ui)
431 428 ui.status('Sending ', m['Subject'], ' ...\n')
432 429 # Exim does not remove the Bcc field
433 430 del m['Bcc']
434 431 fp = cStringIO.StringIO()
435 432 generator = email.Generator.Generator(fp, mangle_from_=False)
436 433 generator.flatten(m, 0)
437 434 sendmail(sender, to + bcc + cc, fp.getvalue())
438 435
439 436 cmdtable = {
440 437 "email":
441 438 (patchbomb,
442 439 [('a', 'attach', None, _('send patches as attachments')),
443 440 ('i', 'inline', None, _('send patches as inline attachments')),
444 441 ('', 'bcc', [], _('email addresses of blind copy recipients')),
445 442 ('c', 'cc', [], _('email addresses of copy recipients')),
446 443 ('d', 'diffstat', None, _('add diffstat output to messages')),
447 444 ('', 'date', '', _('use the given date as the sending date')),
448 445 ('', 'desc', '', _('use the given file as the series description')),
449 446 ('g', 'git', None, _('use git extended diff format')),
450 447 ('f', 'from', '', _('email address of sender')),
451 448 ('', 'plain', None, _('omit hg patch header')),
452 449 ('n', 'test', None, _('print messages that would be sent')),
453 450 ('m', 'mbox', '',
454 451 _('write messages to mbox file instead of sending them')),
455 452 ('o', 'outgoing', None,
456 453 _('send changes not found in the target repository')),
457 454 ('b', 'bundle', None,
458 455 _('send changes not in target as a binary bundle')),
459 456 ('r', 'rev', [], _('a revision to send')),
460 457 ('s', 'subject', '',
461 458 _('subject of first message (intro or single patch)')),
462 459 ('t', 'to', [], _('email addresses of recipients')),
463 460 ('', 'force', None,
464 461 _('run even when remote repository is unrelated (with -b)')),
465 462 ('', 'base', [],
466 463 _('a base changeset to specify instead of a destination (with -b)')),
467 464 ] + commands.remoteopts,
468 465 _('hg email [OPTION]... [DEST]...'))
469 466 }
General Comments 0
You need to be logged in to leave comments. Login now