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