##// END OF EJS Templates
merge with crew-stable
Alexis S. L. Carvalho -
r4096:49237d6a merge default
parent child Browse files
Show More
@@ -1,187 +1,192 b''
1 # extdiff.py - external diff program support for mercurial
1 # extdiff.py - external diff program support for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 #
7 #
8 # The `extdiff' Mercurial extension allows you to use external programs
8 # The `extdiff' Mercurial extension allows you to use external programs
9 # to compare revisions, or revision with working dir. The external diff
9 # to compare revisions, or revision with working dir. The external diff
10 # programs are called with a configurable set of options and two
10 # programs are called with a configurable set of options and two
11 # non-option arguments: paths to directories containing snapshots of
11 # non-option arguments: paths to directories containing snapshots of
12 # files to compare.
12 # files to compare.
13 #
13 #
14 # To enable this extension:
14 # To enable this extension:
15 #
15 #
16 # [extensions]
16 # [extensions]
17 # hgext.extdiff =
17 # hgext.extdiff =
18 #
18 #
19 # The `extdiff' extension also allows to configure new diff commands, so
19 # The `extdiff' extension also allows to configure new diff commands, so
20 # you do not need to type "hg extdiff -p kdiff3" always.
20 # you do not need to type "hg extdiff -p kdiff3" always.
21 #
21 #
22 # [extdiff]
22 # [extdiff]
23 # # add new command that runs GNU diff(1) in 'context diff' mode
23 # # add new command that runs GNU diff(1) in 'context diff' mode
24 # cmd.cdiff = gdiff
24 # cmd.cdiff = gdiff
25 # opts.cdiff = -Nprc5
25 # opts.cdiff = -Nprc5
26
26
27 # # add new command called vdiff, runs kdiff3
27 # # add new command called vdiff, runs kdiff3
28 # cmd.vdiff = kdiff3
28 # cmd.vdiff = kdiff3
29
29
30 # # add new command called meld, runs meld (no need to name twice)
30 # # add new command called meld, runs meld (no need to name twice)
31 # cmd.meld =
31 # cmd.meld =
32
32
33 # # add new command called vimdiff, runs gvimdiff with DirDiff plugin
33 # # add new command called vimdiff, runs gvimdiff with DirDiff plugin
34 # #(see http://www.vim.org/scripts/script.php?script_id=102)
34 # #(see http://www.vim.org/scripts/script.php?script_id=102)
35 # # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
35 # # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
36 # # your .vimrc
36 # # your .vimrc
37 # cmd.vimdiff = gvim
37 # cmd.vimdiff = gvim
38 # opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)'
38 # opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)'
39 #
39 #
40 # Each custom diff commands can have two parts: a `cmd' and an `opts'
40 # Each custom diff commands can have two parts: a `cmd' and an `opts'
41 # part. The cmd.xxx option defines the name of an executable program
41 # part. The cmd.xxx option defines the name of an executable program
42 # that will be run, and opts.xxx defines a set of command-line options
42 # that will be run, and opts.xxx defines a set of command-line options
43 # which will be inserted to the command between the program name and
43 # which will be inserted to the command between the program name and
44 # the files/directories to diff (i.e. the cdiff example above).
44 # the files/directories to diff (i.e. the cdiff example above).
45 #
45 #
46 # You can use -I/-X and list of file or directory names like normal
46 # You can use -I/-X and list of file or directory names like normal
47 # "hg diff" command. The `extdiff' extension makes snapshots of only
47 # "hg diff" command. The `extdiff' extension makes snapshots of only
48 # needed files, so running the external diff program will actually be
48 # needed files, so running the external diff program will actually be
49 # pretty fast (at least faster than having to compare the entire tree).
49 # pretty fast (at least faster than having to compare the entire tree).
50
50
51 from mercurial.i18n import _
51 from mercurial.i18n import _
52 from mercurial.node import *
52 from mercurial.node import *
53 from mercurial import cmdutil, util
53 from mercurial import cmdutil, util
54 import os, shutil, tempfile
54 import os, shutil, tempfile
55
55
56 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
56 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
57 def snapshot_node(files, node):
57 def snapshot_node(files, node):
58 '''snapshot files as of some revision'''
58 '''snapshot files as of some revision'''
59 mf = repo.changectx(node).manifest()
59 mf = repo.changectx(node).manifest()
60 dirname = '%s.%s' % (os.path.basename(repo.root), short(node))
60 dirname = os.path.basename(repo.root)
61 if dirname == "":
62 dirname = "root"
63 dirname = '%s.%s' % (dirname, short(node))
61 base = os.path.join(tmproot, dirname)
64 base = os.path.join(tmproot, dirname)
62 os.mkdir(base)
65 os.mkdir(base)
63 if not ui.quiet:
66 if not ui.quiet:
64 ui.write_err(_('making snapshot of %d files from rev %s\n') %
67 ui.write_err(_('making snapshot of %d files from rev %s\n') %
65 (len(files), short(node)))
68 (len(files), short(node)))
66 for fn in files:
69 for fn in files:
67 if not fn in mf:
70 if not fn in mf:
68 # skipping new file after a merge ?
71 # skipping new file after a merge ?
69 continue
72 continue
70 wfn = util.pconvert(fn)
73 wfn = util.pconvert(fn)
71 ui.note(' %s\n' % wfn)
74 ui.note(' %s\n' % wfn)
72 dest = os.path.join(base, wfn)
75 dest = os.path.join(base, wfn)
73 destdir = os.path.dirname(dest)
76 destdir = os.path.dirname(dest)
74 if not os.path.isdir(destdir):
77 if not os.path.isdir(destdir):
75 os.makedirs(destdir)
78 os.makedirs(destdir)
76 data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn]))
79 data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn]))
77 open(dest, 'w').write(data)
80 open(dest, 'wb').write(data)
78 return dirname
81 return dirname
79
82
80 def snapshot_wdir(files):
83 def snapshot_wdir(files):
81 '''snapshot files from working directory.
84 '''snapshot files from working directory.
82 if not using snapshot, -I/-X does not work and recursive diff
85 if not using snapshot, -I/-X does not work and recursive diff
83 in tools like kdiff3 and meld displays too many files.'''
86 in tools like kdiff3 and meld displays too many files.'''
84 dirname = os.path.basename(repo.root)
87 dirname = os.path.basename(repo.root)
88 if dirname == "":
89 dirname = "root"
85 base = os.path.join(tmproot, dirname)
90 base = os.path.join(tmproot, dirname)
86 os.mkdir(base)
91 os.mkdir(base)
87 if not ui.quiet:
92 if not ui.quiet:
88 ui.write_err(_('making snapshot of %d files from working dir\n') %
93 ui.write_err(_('making snapshot of %d files from working dir\n') %
89 (len(files)))
94 (len(files)))
90 for fn in files:
95 for fn in files:
91 wfn = util.pconvert(fn)
96 wfn = util.pconvert(fn)
92 ui.note(' %s\n' % wfn)
97 ui.note(' %s\n' % wfn)
93 dest = os.path.join(base, wfn)
98 dest = os.path.join(base, wfn)
94 destdir = os.path.dirname(dest)
99 destdir = os.path.dirname(dest)
95 if not os.path.isdir(destdir):
100 if not os.path.isdir(destdir):
96 os.makedirs(destdir)
101 os.makedirs(destdir)
97 fp = open(dest, 'w')
102 fp = open(dest, 'wb')
98 for chunk in util.filechunkiter(repo.wopener(wfn)):
103 for chunk in util.filechunkiter(repo.wopener(wfn)):
99 fp.write(chunk)
104 fp.write(chunk)
100 return dirname
105 return dirname
101
106
102 node1, node2 = cmdutil.revpair(repo, opts['rev'])
107 node1, node2 = cmdutil.revpair(repo, opts['rev'])
103 files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
108 files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
104 modified, added, removed, deleted, unknown = repo.status(
109 modified, added, removed, deleted, unknown = repo.status(
105 node1, node2, files, match=matchfn)[:5]
110 node1, node2, files, match=matchfn)[:5]
106 if not (modified or added or removed):
111 if not (modified or added or removed):
107 return 0
112 return 0
108
113
109 tmproot = tempfile.mkdtemp(prefix='extdiff.')
114 tmproot = tempfile.mkdtemp(prefix='extdiff.')
110 try:
115 try:
111 dir1 = snapshot_node(modified + removed, node1)
116 dir1 = snapshot_node(modified + removed, node1)
112 if node2:
117 if node2:
113 dir2 = snapshot_node(modified + added, node2)
118 dir2 = snapshot_node(modified + added, node2)
114 else:
119 else:
115 dir2 = snapshot_wdir(modified + added)
120 dir2 = snapshot_wdir(modified + added)
116 cmdline = ('%s %s %s %s' %
121 cmdline = ('%s %s %s %s' %
117 (util.shellquote(diffcmd), ' '.join(diffopts),
122 (util.shellquote(diffcmd), ' '.join(diffopts),
118 util.shellquote(dir1), util.shellquote(dir2)))
123 util.shellquote(dir1), util.shellquote(dir2)))
119 ui.debug('running %r in %s\n' % (cmdline, tmproot))
124 ui.debug('running %r in %s\n' % (cmdline, tmproot))
120 util.system(cmdline, cwd=tmproot)
125 util.system(cmdline, cwd=tmproot)
121 return 1
126 return 1
122 finally:
127 finally:
123 ui.note(_('cleaning up temp directory\n'))
128 ui.note(_('cleaning up temp directory\n'))
124 shutil.rmtree(tmproot)
129 shutil.rmtree(tmproot)
125
130
126 def extdiff(ui, repo, *pats, **opts):
131 def extdiff(ui, repo, *pats, **opts):
127 '''use external program to diff repository (or selected files)
132 '''use external program to diff repository (or selected files)
128
133
129 Show differences between revisions for the specified files, using
134 Show differences between revisions for the specified files, using
130 an external program. The default program used is diff, with
135 an external program. The default program used is diff, with
131 default options "-Npru".
136 default options "-Npru".
132
137
133 To select a different program, use the -p option. The program
138 To select a different program, use the -p option. The program
134 will be passed the names of two directories to compare. To pass
139 will be passed the names of two directories to compare. To pass
135 additional options to the program, use the -o option. These will
140 additional options to the program, use the -o option. These will
136 be passed before the names of the directories to compare.
141 be passed before the names of the directories to compare.
137
142
138 When two revision arguments are given, then changes are
143 When two revision arguments are given, then changes are
139 shown between those revisions. If only one revision is
144 shown between those revisions. If only one revision is
140 specified then that revision is compared to the working
145 specified then that revision is compared to the working
141 directory, and, when no revisions are specified, the
146 directory, and, when no revisions are specified, the
142 working directory files are compared to its parent.'''
147 working directory files are compared to its parent.'''
143 program = opts['program'] or 'diff'
148 program = opts['program'] or 'diff'
144 if opts['program']:
149 if opts['program']:
145 option = opts['option']
150 option = opts['option']
146 else:
151 else:
147 option = opts['option'] or ['-Npru']
152 option = opts['option'] or ['-Npru']
148 return dodiff(ui, repo, program, option, pats, opts)
153 return dodiff(ui, repo, program, option, pats, opts)
149
154
150 cmdtable = {
155 cmdtable = {
151 "extdiff":
156 "extdiff":
152 (extdiff,
157 (extdiff,
153 [('p', 'program', '', _('comparison program to run')),
158 [('p', 'program', '', _('comparison program to run')),
154 ('o', 'option', [], _('pass option to comparison program')),
159 ('o', 'option', [], _('pass option to comparison program')),
155 ('r', 'rev', [], _('revision')),
160 ('r', 'rev', [], _('revision')),
156 ('I', 'include', [], _('include names matching the given patterns')),
161 ('I', 'include', [], _('include names matching the given patterns')),
157 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
162 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
158 _('hg extdiff [OPT]... [FILE]...')),
163 _('hg extdiff [OPT]... [FILE]...')),
159 }
164 }
160
165
161 def uisetup(ui):
166 def uisetup(ui):
162 for cmd, path in ui.configitems('extdiff'):
167 for cmd, path in ui.configitems('extdiff'):
163 if not cmd.startswith('cmd.'): continue
168 if not cmd.startswith('cmd.'): continue
164 cmd = cmd[4:]
169 cmd = cmd[4:]
165 if not path: path = cmd
170 if not path: path = cmd
166 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
171 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
167 diffopts = diffopts and [diffopts] or []
172 diffopts = diffopts and [diffopts] or []
168 def save(cmd, path, diffopts):
173 def save(cmd, path, diffopts):
169 '''use closure to save diff command to use'''
174 '''use closure to save diff command to use'''
170 def mydiff(ui, repo, *pats, **opts):
175 def mydiff(ui, repo, *pats, **opts):
171 return dodiff(ui, repo, path, diffopts, pats, opts)
176 return dodiff(ui, repo, path, diffopts, pats, opts)
172 mydiff.__doc__ = '''use %(path)r to diff repository (or selected files)
177 mydiff.__doc__ = '''use %(path)r to diff repository (or selected files)
173
178
174 Show differences between revisions for the specified
179 Show differences between revisions for the specified
175 files, using the %(path)r program.
180 files, using the %(path)r program.
176
181
177 When two revision arguments are given, then changes are
182 When two revision arguments are given, then changes are
178 shown between those revisions. If only one revision is
183 shown between those revisions. If only one revision is
179 specified then that revision is compared to the working
184 specified then that revision is compared to the working
180 directory, and, when no revisions are specified, the
185 directory, and, when no revisions are specified, the
181 working directory files are compared to its parent.''' % {
186 working directory files are compared to its parent.''' % {
182 'path': path,
187 'path': path,
183 }
188 }
184 return mydiff
189 return mydiff
185 cmdtable[cmd] = (save(cmd, path, diffopts),
190 cmdtable[cmd] = (save(cmd, path, diffopts),
186 cmdtable['extdiff'][1][1:],
191 cmdtable['extdiff'][1][1:],
187 _('hg %s [OPT]... [FILE]...') % cmd)
192 _('hg %s [OPT]... [FILE]...') % cmd)
@@ -1,2196 +1,2194 b''
1 # queue.py - patch queues for mercurial
1 # queue.py - patch queues for mercurial
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
8 '''patch management and development
8 '''patch management and development
9
9
10 This extension lets you work with a stack of patches in a Mercurial
10 This extension lets you work with a stack of patches in a Mercurial
11 repository. It manages two stacks of patches - all known patches, and
11 repository. It manages two stacks of patches - all known patches, and
12 applied patches (subset of known patches).
12 applied patches (subset of known patches).
13
13
14 Known patches are represented as patch files in the .hg/patches
14 Known patches are represented as patch files in the .hg/patches
15 directory. Applied patches are both patch files and changesets.
15 directory. Applied patches are both patch files and changesets.
16
16
17 Common tasks (use "hg help command" for more details):
17 Common tasks (use "hg help command" for more details):
18
18
19 prepare repository to work with patches qinit
19 prepare repository to work with patches qinit
20 create new patch qnew
20 create new patch qnew
21 import existing patch qimport
21 import existing patch qimport
22
22
23 print patch series qseries
23 print patch series qseries
24 print applied patches qapplied
24 print applied patches qapplied
25 print name of top applied patch qtop
25 print name of top applied patch qtop
26
26
27 add known patch to applied stack qpush
27 add known patch to applied stack qpush
28 remove patch from applied stack qpop
28 remove patch from applied stack qpop
29 refresh contents of top applied patch qrefresh
29 refresh contents of top applied patch qrefresh
30 '''
30 '''
31
31
32 from mercurial.i18n import _
32 from mercurial.i18n import _
33 from mercurial import commands, cmdutil, hg, patch, revlog, util, changegroup
33 from mercurial import commands, cmdutil, hg, patch, revlog, util, changegroup
34 import os, sys, re, errno
34 import os, sys, re, errno
35
35
36 commands.norepo += " qclone qversion"
36 commands.norepo += " qclone qversion"
37
37
38 # Patch names looks like unix-file names.
38 # Patch names looks like unix-file names.
39 # They must be joinable with queue directory and result in the patch path.
39 # They must be joinable with queue directory and result in the patch path.
40 normname = util.normpath
40 normname = util.normpath
41
41
42 class statusentry:
42 class statusentry:
43 def __init__(self, rev, name=None):
43 def __init__(self, rev, name=None):
44 if not name:
44 if not name:
45 fields = rev.split(':', 1)
45 fields = rev.split(':', 1)
46 if len(fields) == 2:
46 if len(fields) == 2:
47 self.rev, self.name = fields
47 self.rev, self.name = fields
48 else:
48 else:
49 self.rev, self.name = None, None
49 self.rev, self.name = None, None
50 else:
50 else:
51 self.rev, self.name = rev, name
51 self.rev, self.name = rev, name
52
52
53 def __str__(self):
53 def __str__(self):
54 return self.rev + ':' + self.name
54 return self.rev + ':' + self.name
55
55
56 class queue:
56 class queue:
57 def __init__(self, ui, path, patchdir=None):
57 def __init__(self, ui, path, patchdir=None):
58 self.basepath = path
58 self.basepath = path
59 self.path = patchdir or os.path.join(path, "patches")
59 self.path = patchdir or os.path.join(path, "patches")
60 self.opener = util.opener(self.path)
60 self.opener = util.opener(self.path)
61 self.ui = ui
61 self.ui = ui
62 self.applied = []
62 self.applied = []
63 self.full_series = []
63 self.full_series = []
64 self.applied_dirty = 0
64 self.applied_dirty = 0
65 self.series_dirty = 0
65 self.series_dirty = 0
66 self.series_path = "series"
66 self.series_path = "series"
67 self.status_path = "status"
67 self.status_path = "status"
68 self.guards_path = "guards"
68 self.guards_path = "guards"
69 self.active_guards = None
69 self.active_guards = None
70 self.guards_dirty = False
70 self.guards_dirty = False
71 self._diffopts = None
71 self._diffopts = None
72
72
73 if os.path.exists(self.join(self.series_path)):
73 if os.path.exists(self.join(self.series_path)):
74 self.full_series = self.opener(self.series_path).read().splitlines()
74 self.full_series = self.opener(self.series_path).read().splitlines()
75 self.parse_series()
75 self.parse_series()
76
76
77 if os.path.exists(self.join(self.status_path)):
77 if os.path.exists(self.join(self.status_path)):
78 lines = self.opener(self.status_path).read().splitlines()
78 lines = self.opener(self.status_path).read().splitlines()
79 self.applied = [statusentry(l) for l in lines]
79 self.applied = [statusentry(l) for l in lines]
80
80
81 def diffopts(self):
81 def diffopts(self):
82 if self._diffopts is None:
82 if self._diffopts is None:
83 self._diffopts = patch.diffopts(self.ui)
83 self._diffopts = patch.diffopts(self.ui)
84 return self._diffopts
84 return self._diffopts
85
85
86 def join(self, *p):
86 def join(self, *p):
87 return os.path.join(self.path, *p)
87 return os.path.join(self.path, *p)
88
88
89 def find_series(self, patch):
89 def find_series(self, patch):
90 pre = re.compile("(\s*)([^#]+)")
90 pre = re.compile("(\s*)([^#]+)")
91 index = 0
91 index = 0
92 for l in self.full_series:
92 for l in self.full_series:
93 m = pre.match(l)
93 m = pre.match(l)
94 if m:
94 if m:
95 s = m.group(2)
95 s = m.group(2)
96 s = s.rstrip()
96 s = s.rstrip()
97 if s == patch:
97 if s == patch:
98 return index
98 return index
99 index += 1
99 index += 1
100 return None
100 return None
101
101
102 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
102 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
103
103
104 def parse_series(self):
104 def parse_series(self):
105 self.series = []
105 self.series = []
106 self.series_guards = []
106 self.series_guards = []
107 for l in self.full_series:
107 for l in self.full_series:
108 h = l.find('#')
108 h = l.find('#')
109 if h == -1:
109 if h == -1:
110 patch = l
110 patch = l
111 comment = ''
111 comment = ''
112 elif h == 0:
112 elif h == 0:
113 continue
113 continue
114 else:
114 else:
115 patch = l[:h]
115 patch = l[:h]
116 comment = l[h:]
116 comment = l[h:]
117 patch = patch.strip()
117 patch = patch.strip()
118 if patch:
118 if patch:
119 if patch in self.series:
119 if patch in self.series:
120 raise util.Abort(_('%s appears more than once in %s') %
120 raise util.Abort(_('%s appears more than once in %s') %
121 (patch, self.join(self.series_path)))
121 (patch, self.join(self.series_path)))
122 self.series.append(patch)
122 self.series.append(patch)
123 self.series_guards.append(self.guard_re.findall(comment))
123 self.series_guards.append(self.guard_re.findall(comment))
124
124
125 def check_guard(self, guard):
125 def check_guard(self, guard):
126 bad_chars = '# \t\r\n\f'
126 bad_chars = '# \t\r\n\f'
127 first = guard[0]
127 first = guard[0]
128 for c in '-+':
128 for c in '-+':
129 if first == c:
129 if first == c:
130 return (_('guard %r starts with invalid character: %r') %
130 return (_('guard %r starts with invalid character: %r') %
131 (guard, c))
131 (guard, c))
132 for c in bad_chars:
132 for c in bad_chars:
133 if c in guard:
133 if c in guard:
134 return _('invalid character in guard %r: %r') % (guard, c)
134 return _('invalid character in guard %r: %r') % (guard, c)
135
135
136 def set_active(self, guards):
136 def set_active(self, guards):
137 for guard in guards:
137 for guard in guards:
138 bad = self.check_guard(guard)
138 bad = self.check_guard(guard)
139 if bad:
139 if bad:
140 raise util.Abort(bad)
140 raise util.Abort(bad)
141 guards = dict.fromkeys(guards).keys()
141 guards = dict.fromkeys(guards).keys()
142 guards.sort()
142 guards.sort()
143 self.ui.debug('active guards: %s\n' % ' '.join(guards))
143 self.ui.debug('active guards: %s\n' % ' '.join(guards))
144 self.active_guards = guards
144 self.active_guards = guards
145 self.guards_dirty = True
145 self.guards_dirty = True
146
146
147 def active(self):
147 def active(self):
148 if self.active_guards is None:
148 if self.active_guards is None:
149 self.active_guards = []
149 self.active_guards = []
150 try:
150 try:
151 guards = self.opener(self.guards_path).read().split()
151 guards = self.opener(self.guards_path).read().split()
152 except IOError, err:
152 except IOError, err:
153 if err.errno != errno.ENOENT: raise
153 if err.errno != errno.ENOENT: raise
154 guards = []
154 guards = []
155 for i, guard in enumerate(guards):
155 for i, guard in enumerate(guards):
156 bad = self.check_guard(guard)
156 bad = self.check_guard(guard)
157 if bad:
157 if bad:
158 self.ui.warn('%s:%d: %s\n' %
158 self.ui.warn('%s:%d: %s\n' %
159 (self.join(self.guards_path), i + 1, bad))
159 (self.join(self.guards_path), i + 1, bad))
160 else:
160 else:
161 self.active_guards.append(guard)
161 self.active_guards.append(guard)
162 return self.active_guards
162 return self.active_guards
163
163
164 def set_guards(self, idx, guards):
164 def set_guards(self, idx, guards):
165 for g in guards:
165 for g in guards:
166 if len(g) < 2:
166 if len(g) < 2:
167 raise util.Abort(_('guard %r too short') % g)
167 raise util.Abort(_('guard %r too short') % g)
168 if g[0] not in '-+':
168 if g[0] not in '-+':
169 raise util.Abort(_('guard %r starts with invalid char') % g)
169 raise util.Abort(_('guard %r starts with invalid char') % g)
170 bad = self.check_guard(g[1:])
170 bad = self.check_guard(g[1:])
171 if bad:
171 if bad:
172 raise util.Abort(bad)
172 raise util.Abort(bad)
173 drop = self.guard_re.sub('', self.full_series[idx])
173 drop = self.guard_re.sub('', self.full_series[idx])
174 self.full_series[idx] = drop + ''.join([' #' + g for g in guards])
174 self.full_series[idx] = drop + ''.join([' #' + g for g in guards])
175 self.parse_series()
175 self.parse_series()
176 self.series_dirty = True
176 self.series_dirty = True
177
177
178 def pushable(self, idx):
178 def pushable(self, idx):
179 if isinstance(idx, str):
179 if isinstance(idx, str):
180 idx = self.series.index(idx)
180 idx = self.series.index(idx)
181 patchguards = self.series_guards[idx]
181 patchguards = self.series_guards[idx]
182 if not patchguards:
182 if not patchguards:
183 return True, None
183 return True, None
184 default = False
184 default = False
185 guards = self.active()
185 guards = self.active()
186 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
186 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
187 if exactneg:
187 if exactneg:
188 return False, exactneg[0]
188 return False, exactneg[0]
189 pos = [g for g in patchguards if g[0] == '+']
189 pos = [g for g in patchguards if g[0] == '+']
190 exactpos = [g for g in pos if g[1:] in guards]
190 exactpos = [g for g in pos if g[1:] in guards]
191 if pos:
191 if pos:
192 if exactpos:
192 if exactpos:
193 return True, exactpos[0]
193 return True, exactpos[0]
194 return False, pos
194 return False, pos
195 return True, ''
195 return True, ''
196
196
197 def explain_pushable(self, idx, all_patches=False):
197 def explain_pushable(self, idx, all_patches=False):
198 write = all_patches and self.ui.write or self.ui.warn
198 write = all_patches and self.ui.write or self.ui.warn
199 if all_patches or self.ui.verbose:
199 if all_patches or self.ui.verbose:
200 if isinstance(idx, str):
200 if isinstance(idx, str):
201 idx = self.series.index(idx)
201 idx = self.series.index(idx)
202 pushable, why = self.pushable(idx)
202 pushable, why = self.pushable(idx)
203 if all_patches and pushable:
203 if all_patches and pushable:
204 if why is None:
204 if why is None:
205 write(_('allowing %s - no guards in effect\n') %
205 write(_('allowing %s - no guards in effect\n') %
206 self.series[idx])
206 self.series[idx])
207 else:
207 else:
208 if not why:
208 if not why:
209 write(_('allowing %s - no matching negative guards\n') %
209 write(_('allowing %s - no matching negative guards\n') %
210 self.series[idx])
210 self.series[idx])
211 else:
211 else:
212 write(_('allowing %s - guarded by %r\n') %
212 write(_('allowing %s - guarded by %r\n') %
213 (self.series[idx], why))
213 (self.series[idx], why))
214 if not pushable:
214 if not pushable:
215 if why:
215 if why:
216 write(_('skipping %s - guarded by %r\n') %
216 write(_('skipping %s - guarded by %r\n') %
217 (self.series[idx], why))
217 (self.series[idx], why))
218 else:
218 else:
219 write(_('skipping %s - no matching guards\n') %
219 write(_('skipping %s - no matching guards\n') %
220 self.series[idx])
220 self.series[idx])
221
221
222 def save_dirty(self):
222 def save_dirty(self):
223 def write_list(items, path):
223 def write_list(items, path):
224 fp = self.opener(path, 'w')
224 fp = self.opener(path, 'w')
225 for i in items:
225 for i in items:
226 print >> fp, i
226 print >> fp, i
227 fp.close()
227 fp.close()
228 if self.applied_dirty: write_list(map(str, self.applied), self.status_path)
228 if self.applied_dirty: write_list(map(str, self.applied), self.status_path)
229 if self.series_dirty: write_list(self.full_series, self.series_path)
229 if self.series_dirty: write_list(self.full_series, self.series_path)
230 if self.guards_dirty: write_list(self.active_guards, self.guards_path)
230 if self.guards_dirty: write_list(self.active_guards, self.guards_path)
231
231
232 def readheaders(self, patch):
232 def readheaders(self, patch):
233 def eatdiff(lines):
233 def eatdiff(lines):
234 while lines:
234 while lines:
235 l = lines[-1]
235 l = lines[-1]
236 if (l.startswith("diff -") or
236 if (l.startswith("diff -") or
237 l.startswith("Index:") or
237 l.startswith("Index:") or
238 l.startswith("===========")):
238 l.startswith("===========")):
239 del lines[-1]
239 del lines[-1]
240 else:
240 else:
241 break
241 break
242 def eatempty(lines):
242 def eatempty(lines):
243 while lines:
243 while lines:
244 l = lines[-1]
244 l = lines[-1]
245 if re.match('\s*$', l):
245 if re.match('\s*$', l):
246 del lines[-1]
246 del lines[-1]
247 else:
247 else:
248 break
248 break
249
249
250 pf = self.join(patch)
250 pf = self.join(patch)
251 message = []
251 message = []
252 comments = []
252 comments = []
253 user = None
253 user = None
254 date = None
254 date = None
255 format = None
255 format = None
256 subject = None
256 subject = None
257 diffstart = 0
257 diffstart = 0
258
258
259 for line in file(pf):
259 for line in file(pf):
260 line = line.rstrip()
260 line = line.rstrip()
261 if line.startswith('diff --git'):
261 if line.startswith('diff --git'):
262 diffstart = 2
262 diffstart = 2
263 break
263 break
264 if diffstart:
264 if diffstart:
265 if line.startswith('+++ '):
265 if line.startswith('+++ '):
266 diffstart = 2
266 diffstart = 2
267 break
267 break
268 if line.startswith("--- "):
268 if line.startswith("--- "):
269 diffstart = 1
269 diffstart = 1
270 continue
270 continue
271 elif format == "hgpatch":
271 elif format == "hgpatch":
272 # parse values when importing the result of an hg export
272 # parse values when importing the result of an hg export
273 if line.startswith("# User "):
273 if line.startswith("# User "):
274 user = line[7:]
274 user = line[7:]
275 elif line.startswith("# Date "):
275 elif line.startswith("# Date "):
276 date = line[7:]
276 date = line[7:]
277 elif not line.startswith("# ") and line:
277 elif not line.startswith("# ") and line:
278 message.append(line)
278 message.append(line)
279 format = None
279 format = None
280 elif line == '# HG changeset patch':
280 elif line == '# HG changeset patch':
281 format = "hgpatch"
281 format = "hgpatch"
282 elif (format != "tagdone" and (line.startswith("Subject: ") or
282 elif (format != "tagdone" and (line.startswith("Subject: ") or
283 line.startswith("subject: "))):
283 line.startswith("subject: "))):
284 subject = line[9:]
284 subject = line[9:]
285 format = "tag"
285 format = "tag"
286 elif (format != "tagdone" and (line.startswith("From: ") or
286 elif (format != "tagdone" and (line.startswith("From: ") or
287 line.startswith("from: "))):
287 line.startswith("from: "))):
288 user = line[6:]
288 user = line[6:]
289 format = "tag"
289 format = "tag"
290 elif format == "tag" and line == "":
290 elif format == "tag" and line == "":
291 # when looking for tags (subject: from: etc) they
291 # when looking for tags (subject: from: etc) they
292 # end once you find a blank line in the source
292 # end once you find a blank line in the source
293 format = "tagdone"
293 format = "tagdone"
294 elif message or line:
294 elif message or line:
295 message.append(line)
295 message.append(line)
296 comments.append(line)
296 comments.append(line)
297
297
298 eatdiff(message)
298 eatdiff(message)
299 eatdiff(comments)
299 eatdiff(comments)
300 eatempty(message)
300 eatempty(message)
301 eatempty(comments)
301 eatempty(comments)
302
302
303 # make sure message isn't empty
303 # make sure message isn't empty
304 if format and format.startswith("tag") and subject:
304 if format and format.startswith("tag") and subject:
305 message.insert(0, "")
305 message.insert(0, "")
306 message.insert(0, subject)
306 message.insert(0, subject)
307 return (message, comments, user, date, diffstart > 1)
307 return (message, comments, user, date, diffstart > 1)
308
308
309 def printdiff(self, repo, node1, node2=None, files=None,
309 def printdiff(self, repo, node1, node2=None, files=None,
310 fp=None, changes=None, opts={}):
310 fp=None, changes=None, opts={}):
311 fns, matchfn, anypats = cmdutil.matchpats(repo, files, opts)
311 fns, matchfn, anypats = cmdutil.matchpats(repo, files, opts)
312
312
313 patch.diff(repo, node1, node2, fns, match=matchfn,
313 patch.diff(repo, node1, node2, fns, match=matchfn,
314 fp=fp, changes=changes, opts=self.diffopts())
314 fp=fp, changes=changes, opts=self.diffopts())
315
315
316 def mergeone(self, repo, mergeq, head, patch, rev, wlock):
316 def mergeone(self, repo, mergeq, head, patch, rev, wlock):
317 # first try just applying the patch
317 # first try just applying the patch
318 (err, n) = self.apply(repo, [ patch ], update_status=False,
318 (err, n) = self.apply(repo, [ patch ], update_status=False,
319 strict=True, merge=rev, wlock=wlock)
319 strict=True, merge=rev, wlock=wlock)
320
320
321 if err == 0:
321 if err == 0:
322 return (err, n)
322 return (err, n)
323
323
324 if n is None:
324 if n is None:
325 raise util.Abort(_("apply failed for patch %s") % patch)
325 raise util.Abort(_("apply failed for patch %s") % patch)
326
326
327 self.ui.warn("patch didn't work out, merging %s\n" % patch)
327 self.ui.warn("patch didn't work out, merging %s\n" % patch)
328
328
329 # apply failed, strip away that rev and merge.
329 # apply failed, strip away that rev and merge.
330 hg.clean(repo, head, wlock=wlock)
330 hg.clean(repo, head, wlock=wlock)
331 self.strip(repo, n, update=False, backup='strip', wlock=wlock)
331 self.strip(repo, n, update=False, backup='strip', wlock=wlock)
332
332
333 ctx = repo.changectx(rev)
333 ctx = repo.changectx(rev)
334 ret = hg.merge(repo, rev, wlock=wlock)
334 ret = hg.merge(repo, rev, wlock=wlock)
335 if ret:
335 if ret:
336 raise util.Abort(_("update returned %d") % ret)
336 raise util.Abort(_("update returned %d") % ret)
337 n = repo.commit(None, ctx.description(), ctx.user(),
337 n = repo.commit(None, ctx.description(), ctx.user(),
338 force=1, wlock=wlock)
338 force=1, wlock=wlock)
339 if n == None:
339 if n == None:
340 raise util.Abort(_("repo commit failed"))
340 raise util.Abort(_("repo commit failed"))
341 try:
341 try:
342 message, comments, user, date, patchfound = mergeq.readheaders(patch)
342 message, comments, user, date, patchfound = mergeq.readheaders(patch)
343 except:
343 except:
344 raise util.Abort(_("unable to read %s") % patch)
344 raise util.Abort(_("unable to read %s") % patch)
345
345
346 patchf = self.opener(patch, "w")
346 patchf = self.opener(patch, "w")
347 if comments:
347 if comments:
348 comments = "\n".join(comments) + '\n\n'
348 comments = "\n".join(comments) + '\n\n'
349 patchf.write(comments)
349 patchf.write(comments)
350 self.printdiff(repo, head, n, fp=patchf)
350 self.printdiff(repo, head, n, fp=patchf)
351 patchf.close()
351 patchf.close()
352 return (0, n)
352 return (0, n)
353
353
354 def qparents(self, repo, rev=None):
354 def qparents(self, repo, rev=None):
355 if rev is None:
355 if rev is None:
356 (p1, p2) = repo.dirstate.parents()
356 (p1, p2) = repo.dirstate.parents()
357 if p2 == revlog.nullid:
357 if p2 == revlog.nullid:
358 return p1
358 return p1
359 if len(self.applied) == 0:
359 if len(self.applied) == 0:
360 return None
360 return None
361 return revlog.bin(self.applied[-1].rev)
361 return revlog.bin(self.applied[-1].rev)
362 pp = repo.changelog.parents(rev)
362 pp = repo.changelog.parents(rev)
363 if pp[1] != revlog.nullid:
363 if pp[1] != revlog.nullid:
364 arevs = [ x.rev for x in self.applied ]
364 arevs = [ x.rev for x in self.applied ]
365 p0 = revlog.hex(pp[0])
365 p0 = revlog.hex(pp[0])
366 p1 = revlog.hex(pp[1])
366 p1 = revlog.hex(pp[1])
367 if p0 in arevs:
367 if p0 in arevs:
368 return pp[0]
368 return pp[0]
369 if p1 in arevs:
369 if p1 in arevs:
370 return pp[1]
370 return pp[1]
371 return pp[0]
371 return pp[0]
372
372
373 def mergepatch(self, repo, mergeq, series, wlock):
373 def mergepatch(self, repo, mergeq, series, wlock):
374 if len(self.applied) == 0:
374 if len(self.applied) == 0:
375 # each of the patches merged in will have two parents. This
375 # each of the patches merged in will have two parents. This
376 # can confuse the qrefresh, qdiff, and strip code because it
376 # can confuse the qrefresh, qdiff, and strip code because it
377 # needs to know which parent is actually in the patch queue.
377 # needs to know which parent is actually in the patch queue.
378 # so, we insert a merge marker with only one parent. This way
378 # so, we insert a merge marker with only one parent. This way
379 # the first patch in the queue is never a merge patch
379 # the first patch in the queue is never a merge patch
380 #
380 #
381 pname = ".hg.patches.merge.marker"
381 pname = ".hg.patches.merge.marker"
382 n = repo.commit(None, '[mq]: merge marker', user=None, force=1,
382 n = repo.commit(None, '[mq]: merge marker', user=None, force=1,
383 wlock=wlock)
383 wlock=wlock)
384 self.applied.append(statusentry(revlog.hex(n), pname))
384 self.applied.append(statusentry(revlog.hex(n), pname))
385 self.applied_dirty = 1
385 self.applied_dirty = 1
386
386
387 head = self.qparents(repo)
387 head = self.qparents(repo)
388
388
389 for patch in series:
389 for patch in series:
390 patch = mergeq.lookup(patch, strict=True)
390 patch = mergeq.lookup(patch, strict=True)
391 if not patch:
391 if not patch:
392 self.ui.warn("patch %s does not exist\n" % patch)
392 self.ui.warn("patch %s does not exist\n" % patch)
393 return (1, None)
393 return (1, None)
394 pushable, reason = self.pushable(patch)
394 pushable, reason = self.pushable(patch)
395 if not pushable:
395 if not pushable:
396 self.explain_pushable(patch, all_patches=True)
396 self.explain_pushable(patch, all_patches=True)
397 continue
397 continue
398 info = mergeq.isapplied(patch)
398 info = mergeq.isapplied(patch)
399 if not info:
399 if not info:
400 self.ui.warn("patch %s is not applied\n" % patch)
400 self.ui.warn("patch %s is not applied\n" % patch)
401 return (1, None)
401 return (1, None)
402 rev = revlog.bin(info[1])
402 rev = revlog.bin(info[1])
403 (err, head) = self.mergeone(repo, mergeq, head, patch, rev, wlock)
403 (err, head) = self.mergeone(repo, mergeq, head, patch, rev, wlock)
404 if head:
404 if head:
405 self.applied.append(statusentry(revlog.hex(head), patch))
405 self.applied.append(statusentry(revlog.hex(head), patch))
406 self.applied_dirty = 1
406 self.applied_dirty = 1
407 if err:
407 if err:
408 return (err, head)
408 return (err, head)
409 return (0, head)
409 return (0, head)
410
410
411 def patch(self, repo, patchfile):
411 def patch(self, repo, patchfile):
412 '''Apply patchfile to the working directory.
412 '''Apply patchfile to the working directory.
413 patchfile: file name of patch'''
413 patchfile: file name of patch'''
414 files = {}
414 files = {}
415 try:
415 try:
416 fuzz = patch.patch(patchfile, self.ui, strip=1, cwd=repo.root,
416 fuzz = patch.patch(patchfile, self.ui, strip=1, cwd=repo.root,
417 files=files)
417 files=files)
418 except Exception, inst:
418 except Exception, inst:
419 self.ui.note(str(inst) + '\n')
419 self.ui.note(str(inst) + '\n')
420 if not self.ui.verbose:
420 if not self.ui.verbose:
421 self.ui.warn("patch failed, unable to continue (try -v)\n")
421 self.ui.warn("patch failed, unable to continue (try -v)\n")
422 return (False, files, False)
422 return (False, files, False)
423
423
424 return (True, files, fuzz)
424 return (True, files, fuzz)
425
425
426 def apply(self, repo, series, list=False, update_status=True,
426 def apply(self, repo, series, list=False, update_status=True,
427 strict=False, patchdir=None, merge=None, wlock=None):
427 strict=False, patchdir=None, merge=None, wlock=None):
428 # TODO unify with commands.py
428 # TODO unify with commands.py
429 if not patchdir:
429 if not patchdir:
430 patchdir = self.path
430 patchdir = self.path
431 err = 0
431 err = 0
432 if not wlock:
432 if not wlock:
433 wlock = repo.wlock()
433 wlock = repo.wlock()
434 lock = repo.lock()
434 lock = repo.lock()
435 tr = repo.transaction()
435 tr = repo.transaction()
436 n = None
436 n = None
437 for patchname in series:
437 for patchname in series:
438 pushable, reason = self.pushable(patchname)
438 pushable, reason = self.pushable(patchname)
439 if not pushable:
439 if not pushable:
440 self.explain_pushable(patchname, all_patches=True)
440 self.explain_pushable(patchname, all_patches=True)
441 continue
441 continue
442 self.ui.warn("applying %s\n" % patchname)
442 self.ui.warn("applying %s\n" % patchname)
443 pf = os.path.join(patchdir, patchname)
443 pf = os.path.join(patchdir, patchname)
444
444
445 try:
445 try:
446 message, comments, user, date, patchfound = self.readheaders(patchname)
446 message, comments, user, date, patchfound = self.readheaders(patchname)
447 except:
447 except:
448 self.ui.warn("Unable to read %s\n" % patchname)
448 self.ui.warn("Unable to read %s\n" % patchname)
449 err = 1
449 err = 1
450 break
450 break
451
451
452 if not message:
452 if not message:
453 message = "imported patch %s\n" % patchname
453 message = "imported patch %s\n" % patchname
454 else:
454 else:
455 if list:
455 if list:
456 message.append("\nimported patch %s" % patchname)
456 message.append("\nimported patch %s" % patchname)
457 message = '\n'.join(message)
457 message = '\n'.join(message)
458
458
459 (patcherr, files, fuzz) = self.patch(repo, pf)
459 (patcherr, files, fuzz) = self.patch(repo, pf)
460 patcherr = not patcherr
460 patcherr = not patcherr
461
461
462 if merge and files:
462 if merge and files:
463 # Mark as merged and update dirstate parent info
463 # Mark as merged and update dirstate parent info
464 repo.dirstate.update(repo.dirstate.filterfiles(files.keys()), 'm')
464 repo.dirstate.update(repo.dirstate.filterfiles(files.keys()), 'm')
465 p1, p2 = repo.dirstate.parents()
465 p1, p2 = repo.dirstate.parents()
466 repo.dirstate.setparents(p1, merge)
466 repo.dirstate.setparents(p1, merge)
467 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
467 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
468 n = repo.commit(files, message, user, date, force=1, lock=lock,
468 n = repo.commit(files, message, user, date, force=1, lock=lock,
469 wlock=wlock)
469 wlock=wlock)
470
470
471 if n == None:
471 if n == None:
472 raise util.Abort(_("repo commit failed"))
472 raise util.Abort(_("repo commit failed"))
473
473
474 if update_status:
474 if update_status:
475 self.applied.append(statusentry(revlog.hex(n), patchname))
475 self.applied.append(statusentry(revlog.hex(n), patchname))
476
476
477 if patcherr:
477 if patcherr:
478 if not patchfound:
478 if not patchfound:
479 self.ui.warn("patch %s is empty\n" % patchname)
479 self.ui.warn("patch %s is empty\n" % patchname)
480 err = 0
480 err = 0
481 else:
481 else:
482 self.ui.warn("patch failed, rejects left in working dir\n")
482 self.ui.warn("patch failed, rejects left in working dir\n")
483 err = 1
483 err = 1
484 break
484 break
485
485
486 if fuzz and strict:
486 if fuzz and strict:
487 self.ui.warn("fuzz found when applying patch, stopping\n")
487 self.ui.warn("fuzz found when applying patch, stopping\n")
488 err = 1
488 err = 1
489 break
489 break
490 tr.close()
490 tr.close()
491 return (err, n)
491 return (err, n)
492
492
493 def delete(self, repo, patches, opts):
493 def delete(self, repo, patches, opts):
494 realpatches = []
494 realpatches = []
495 for patch in patches:
495 for patch in patches:
496 patch = self.lookup(patch, strict=True)
496 patch = self.lookup(patch, strict=True)
497 info = self.isapplied(patch)
497 info = self.isapplied(patch)
498 if info:
498 if info:
499 raise util.Abort(_("cannot delete applied patch %s") % patch)
499 raise util.Abort(_("cannot delete applied patch %s") % patch)
500 if patch not in self.series:
500 if patch not in self.series:
501 raise util.Abort(_("patch %s not in series file") % patch)
501 raise util.Abort(_("patch %s not in series file") % patch)
502 realpatches.append(patch)
502 realpatches.append(patch)
503
503
504 appliedbase = 0
504 appliedbase = 0
505 if opts.get('rev'):
505 if opts.get('rev'):
506 if not self.applied:
506 if not self.applied:
507 raise util.Abort(_('no patches applied'))
507 raise util.Abort(_('no patches applied'))
508 revs = cmdutil.revrange(repo, opts['rev'])
508 revs = cmdutil.revrange(repo, opts['rev'])
509 if len(revs) > 1 and revs[0] > revs[1]:
509 if len(revs) > 1 and revs[0] > revs[1]:
510 revs.reverse()
510 revs.reverse()
511 for rev in revs:
511 for rev in revs:
512 if appliedbase >= len(self.applied):
512 if appliedbase >= len(self.applied):
513 raise util.Abort(_("revision %d is not managed") % rev)
513 raise util.Abort(_("revision %d is not managed") % rev)
514
514
515 base = revlog.bin(self.applied[appliedbase].rev)
515 base = revlog.bin(self.applied[appliedbase].rev)
516 node = repo.changelog.node(rev)
516 node = repo.changelog.node(rev)
517 if node != base:
517 if node != base:
518 raise util.Abort(_("cannot delete revision %d above "
518 raise util.Abort(_("cannot delete revision %d above "
519 "applied patches") % rev)
519 "applied patches") % rev)
520 realpatches.append(self.applied[appliedbase].name)
520 realpatches.append(self.applied[appliedbase].name)
521 appliedbase += 1
521 appliedbase += 1
522
522
523 if not opts.get('keep'):
523 if not opts.get('keep'):
524 r = self.qrepo()
524 r = self.qrepo()
525 if r:
525 if r:
526 r.remove(realpatches, True)
526 r.remove(realpatches, True)
527 else:
527 else:
528 for p in realpatches:
528 for p in realpatches:
529 os.unlink(self.join(p))
529 os.unlink(self.join(p))
530
530
531 if appliedbase:
531 if appliedbase:
532 del self.applied[:appliedbase]
532 del self.applied[:appliedbase]
533 self.applied_dirty = 1
533 self.applied_dirty = 1
534 indices = [self.find_series(p) for p in realpatches]
534 indices = [self.find_series(p) for p in realpatches]
535 indices.sort()
535 indices.sort()
536 for i in indices[-1::-1]:
536 for i in indices[-1::-1]:
537 del self.full_series[i]
537 del self.full_series[i]
538 self.parse_series()
538 self.parse_series()
539 self.series_dirty = 1
539 self.series_dirty = 1
540
540
541 def check_toppatch(self, repo):
541 def check_toppatch(self, repo):
542 if len(self.applied) > 0:
542 if len(self.applied) > 0:
543 top = revlog.bin(self.applied[-1].rev)
543 top = revlog.bin(self.applied[-1].rev)
544 pp = repo.dirstate.parents()
544 pp = repo.dirstate.parents()
545 if top not in pp:
545 if top not in pp:
546 raise util.Abort(_("queue top not at same revision as working directory"))
546 raise util.Abort(_("queue top not at same revision as working directory"))
547 return top
547 return top
548 return None
548 return None
549 def check_localchanges(self, repo, force=False, refresh=True):
549 def check_localchanges(self, repo, force=False, refresh=True):
550 m, a, r, d = repo.status()[:4]
550 m, a, r, d = repo.status()[:4]
551 if m or a or r or d:
551 if m or a or r or d:
552 if not force:
552 if not force:
553 if refresh:
553 if refresh:
554 raise util.Abort(_("local changes found, refresh first"))
554 raise util.Abort(_("local changes found, refresh first"))
555 else:
555 else:
556 raise util.Abort(_("local changes found"))
556 raise util.Abort(_("local changes found"))
557 return m, a, r, d
557 return m, a, r, d
558 def new(self, repo, patch, msg=None, force=None):
558 def new(self, repo, patch, msg=None, force=None):
559 if os.path.exists(self.join(patch)):
559 if os.path.exists(self.join(patch)):
560 raise util.Abort(_('patch "%s" already exists') % patch)
560 raise util.Abort(_('patch "%s" already exists') % patch)
561 m, a, r, d = self.check_localchanges(repo, force)
561 m, a, r, d = self.check_localchanges(repo, force)
562 commitfiles = m + a + r
562 commitfiles = m + a + r
563 self.check_toppatch(repo)
563 self.check_toppatch(repo)
564 wlock = repo.wlock()
564 wlock = repo.wlock()
565 insert = self.full_series_end()
565 insert = self.full_series_end()
566 if msg:
566 if msg:
567 n = repo.commit(commitfiles, "[mq]: %s" % msg, force=True,
567 n = repo.commit(commitfiles, "[mq]: %s" % msg, force=True,
568 wlock=wlock)
568 wlock=wlock)
569 else:
569 else:
570 n = repo.commit(commitfiles,
570 n = repo.commit(commitfiles,
571 "New patch: %s" % patch, force=True, wlock=wlock)
571 "New patch: %s" % patch, force=True, wlock=wlock)
572 if n == None:
572 if n == None:
573 raise util.Abort(_("repo commit failed"))
573 raise util.Abort(_("repo commit failed"))
574 self.full_series[insert:insert] = [patch]
574 self.full_series[insert:insert] = [patch]
575 self.applied.append(statusentry(revlog.hex(n), patch))
575 self.applied.append(statusentry(revlog.hex(n), patch))
576 self.parse_series()
576 self.parse_series()
577 self.series_dirty = 1
577 self.series_dirty = 1
578 self.applied_dirty = 1
578 self.applied_dirty = 1
579 p = self.opener(patch, "w")
579 p = self.opener(patch, "w")
580 if msg:
580 if msg:
581 msg = msg + "\n"
581 msg = msg + "\n"
582 p.write(msg)
582 p.write(msg)
583 p.close()
583 p.close()
584 wlock = None
584 wlock = None
585 r = self.qrepo()
585 r = self.qrepo()
586 if r: r.add([patch])
586 if r: r.add([patch])
587 if commitfiles:
587 if commitfiles:
588 self.refresh(repo, short=True)
588 self.refresh(repo, short=True)
589
589
590 def strip(self, repo, rev, update=True, backup="all", wlock=None):
590 def strip(self, repo, rev, update=True, backup="all", wlock=None):
591 def limitheads(chlog, stop):
591 def limitheads(chlog, stop):
592 """return the list of all nodes that have no children"""
592 """return the list of all nodes that have no children"""
593 p = {}
593 p = {}
594 h = []
594 h = []
595 stoprev = 0
595 stoprev = 0
596 if stop in chlog.nodemap:
596 if stop in chlog.nodemap:
597 stoprev = chlog.rev(stop)
597 stoprev = chlog.rev(stop)
598
598
599 for r in xrange(chlog.count() - 1, -1, -1):
599 for r in xrange(chlog.count() - 1, -1, -1):
600 n = chlog.node(r)
600 n = chlog.node(r)
601 if n not in p:
601 if n not in p:
602 h.append(n)
602 h.append(n)
603 if n == stop:
603 if n == stop:
604 break
604 break
605 if r < stoprev:
605 if r < stoprev:
606 break
606 break
607 for pn in chlog.parents(n):
607 for pn in chlog.parents(n):
608 p[pn] = 1
608 p[pn] = 1
609 return h
609 return h
610
610
611 def bundle(cg):
611 def bundle(cg):
612 backupdir = repo.join("strip-backup")
612 backupdir = repo.join("strip-backup")
613 if not os.path.isdir(backupdir):
613 if not os.path.isdir(backupdir):
614 os.mkdir(backupdir)
614 os.mkdir(backupdir)
615 name = os.path.join(backupdir, "%s" % revlog.short(rev))
615 name = os.path.join(backupdir, "%s" % revlog.short(rev))
616 name = savename(name)
616 name = savename(name)
617 self.ui.warn("saving bundle to %s\n" % name)
617 self.ui.warn("saving bundle to %s\n" % name)
618 return changegroup.writebundle(cg, name, "HG10BZ")
618 return changegroup.writebundle(cg, name, "HG10BZ")
619
619
620 def stripall(revnum):
620 def stripall(revnum):
621 mm = repo.changectx(rev).manifest()
621 mm = repo.changectx(rev).manifest()
622 seen = {}
622 seen = {}
623
623
624 for x in xrange(revnum, repo.changelog.count()):
624 for x in xrange(revnum, repo.changelog.count()):
625 for f in repo.changectx(x).files():
625 for f in repo.changectx(x).files():
626 if f in seen:
626 if f in seen:
627 continue
627 continue
628 seen[f] = 1
628 seen[f] = 1
629 if f in mm:
629 if f in mm:
630 filerev = mm[f]
630 filerev = mm[f]
631 else:
631 else:
632 filerev = 0
632 filerev = 0
633 seen[f] = filerev
633 seen[f] = filerev
634 # we go in two steps here so the strip loop happens in a
634 # we go in two steps here so the strip loop happens in a
635 # sensible order. When stripping many files, this helps keep
635 # sensible order. When stripping many files, this helps keep
636 # our disk access patterns under control.
636 # our disk access patterns under control.
637 seen_list = seen.keys()
637 seen_list = seen.keys()
638 seen_list.sort()
638 seen_list.sort()
639 for f in seen_list:
639 for f in seen_list:
640 ff = repo.file(f)
640 ff = repo.file(f)
641 filerev = seen[f]
641 filerev = seen[f]
642 if filerev != 0:
642 if filerev != 0:
643 if filerev in ff.nodemap:
643 if filerev in ff.nodemap:
644 filerev = ff.rev(filerev)
644 filerev = ff.rev(filerev)
645 else:
645 else:
646 filerev = 0
646 filerev = 0
647 ff.strip(filerev, revnum)
647 ff.strip(filerev, revnum)
648
648
649 if not wlock:
649 if not wlock:
650 wlock = repo.wlock()
650 wlock = repo.wlock()
651 lock = repo.lock()
651 lock = repo.lock()
652 chlog = repo.changelog
652 chlog = repo.changelog
653 # TODO delete the undo files, and handle undo of merge sets
653 # TODO delete the undo files, and handle undo of merge sets
654 pp = chlog.parents(rev)
654 pp = chlog.parents(rev)
655 revnum = chlog.rev(rev)
655 revnum = chlog.rev(rev)
656
656
657 if update:
657 if update:
658 self.check_localchanges(repo, refresh=False)
658 self.check_localchanges(repo, refresh=False)
659 urev = self.qparents(repo, rev)
659 urev = self.qparents(repo, rev)
660 hg.clean(repo, urev, wlock=wlock)
660 hg.clean(repo, urev, wlock=wlock)
661 repo.dirstate.write()
661 repo.dirstate.write()
662
662
663 # save is a list of all the branches we are truncating away
663 # save is a list of all the branches we are truncating away
664 # that we actually want to keep. changegroup will be used
664 # that we actually want to keep. changegroup will be used
665 # to preserve them and add them back after the truncate
665 # to preserve them and add them back after the truncate
666 saveheads = []
666 saveheads = []
667 savebases = {}
667 savebases = {}
668
668
669 heads = limitheads(chlog, rev)
669 heads = limitheads(chlog, rev)
670 seen = {}
670 seen = {}
671
671
672 # search through all the heads, finding those where the revision
672 # search through all the heads, finding those where the revision
673 # we want to strip away is an ancestor. Also look for merges
673 # we want to strip away is an ancestor. Also look for merges
674 # that might be turned into new heads by the strip.
674 # that might be turned into new heads by the strip.
675 while heads:
675 while heads:
676 h = heads.pop()
676 h = heads.pop()
677 n = h
677 n = h
678 while True:
678 while True:
679 seen[n] = 1
679 seen[n] = 1
680 pp = chlog.parents(n)
680 pp = chlog.parents(n)
681 if pp[1] != revlog.nullid:
681 if pp[1] != revlog.nullid:
682 for p in pp:
682 for p in pp:
683 if chlog.rev(p) > revnum and p not in seen:
683 if chlog.rev(p) > revnum and p not in seen:
684 heads.append(p)
684 heads.append(p)
685 if pp[0] == revlog.nullid:
685 if pp[0] == revlog.nullid:
686 break
686 break
687 if chlog.rev(pp[0]) < revnum:
687 if chlog.rev(pp[0]) < revnum:
688 break
688 break
689 n = pp[0]
689 n = pp[0]
690 if n == rev:
690 if n == rev:
691 break
691 break
692 r = chlog.reachable(h, rev)
692 r = chlog.reachable(h, rev)
693 if rev not in r:
693 if rev not in r:
694 saveheads.append(h)
694 saveheads.append(h)
695 for x in r:
695 for x in r:
696 if chlog.rev(x) > revnum:
696 if chlog.rev(x) > revnum:
697 savebases[x] = 1
697 savebases[x] = 1
698
698
699 # create a changegroup for all the branches we need to keep
699 # create a changegroup for all the branches we need to keep
700 if backup == "all":
700 if backup == "all":
701 backupch = repo.changegroupsubset([rev], chlog.heads(), 'strip')
701 backupch = repo.changegroupsubset([rev], chlog.heads(), 'strip')
702 bundle(backupch)
702 bundle(backupch)
703 if saveheads:
703 if saveheads:
704 backupch = repo.changegroupsubset(savebases.keys(), saveheads, 'strip')
704 backupch = repo.changegroupsubset(savebases.keys(), saveheads, 'strip')
705 chgrpfile = bundle(backupch)
705 chgrpfile = bundle(backupch)
706
706
707 stripall(revnum)
707 stripall(revnum)
708
708
709 change = chlog.read(rev)
709 change = chlog.read(rev)
710 chlog.strip(revnum, revnum)
710 chlog.strip(revnum, revnum)
711 repo.manifest.strip(repo.manifest.rev(change[0]), revnum)
711 repo.manifest.strip(repo.manifest.rev(change[0]), revnum)
712 if saveheads:
712 if saveheads:
713 self.ui.status("adding branch\n")
713 self.ui.status("adding branch\n")
714 commands.unbundle(self.ui, repo, "file:%s" % chgrpfile,
714 commands.unbundle(self.ui, repo, "file:%s" % chgrpfile,
715 update=False)
715 update=False)
716 if backup != "strip":
716 if backup != "strip":
717 os.unlink(chgrpfile)
717 os.unlink(chgrpfile)
718
718
719 def isapplied(self, patch):
719 def isapplied(self, patch):
720 """returns (index, rev, patch)"""
720 """returns (index, rev, patch)"""
721 for i in xrange(len(self.applied)):
721 for i in xrange(len(self.applied)):
722 a = self.applied[i]
722 a = self.applied[i]
723 if a.name == patch:
723 if a.name == patch:
724 return (i, a.rev, a.name)
724 return (i, a.rev, a.name)
725 return None
725 return None
726
726
727 # if the exact patch name does not exist, we try a few
727 # if the exact patch name does not exist, we try a few
728 # variations. If strict is passed, we try only #1
728 # variations. If strict is passed, we try only #1
729 #
729 #
730 # 1) a number to indicate an offset in the series file
730 # 1) a number to indicate an offset in the series file
731 # 2) a unique substring of the patch name was given
731 # 2) a unique substring of the patch name was given
732 # 3) patchname[-+]num to indicate an offset in the series file
732 # 3) patchname[-+]num to indicate an offset in the series file
733 def lookup(self, patch, strict=False):
733 def lookup(self, patch, strict=False):
734 patch = patch and str(patch)
734 patch = patch and str(patch)
735
735
736 def partial_name(s):
736 def partial_name(s):
737 if s in self.series:
737 if s in self.series:
738 return s
738 return s
739 matches = [x for x in self.series if s in x]
739 matches = [x for x in self.series if s in x]
740 if len(matches) > 1:
740 if len(matches) > 1:
741 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
741 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
742 for m in matches:
742 for m in matches:
743 self.ui.warn(' %s\n' % m)
743 self.ui.warn(' %s\n' % m)
744 return None
744 return None
745 if matches:
745 if matches:
746 return matches[0]
746 return matches[0]
747 if len(self.series) > 0 and len(self.applied) > 0:
747 if len(self.series) > 0 and len(self.applied) > 0:
748 if s == 'qtip':
748 if s == 'qtip':
749 return self.series[self.series_end(True)-1]
749 return self.series[self.series_end(True)-1]
750 if s == 'qbase':
750 if s == 'qbase':
751 return self.series[0]
751 return self.series[0]
752 return None
752 return None
753 if patch == None:
753 if patch == None:
754 return None
754 return None
755
755
756 # we don't want to return a partial match until we make
756 # we don't want to return a partial match until we make
757 # sure the file name passed in does not exist (checked below)
757 # sure the file name passed in does not exist (checked below)
758 res = partial_name(patch)
758 res = partial_name(patch)
759 if res and res == patch:
759 if res and res == patch:
760 return res
760 return res
761
761
762 if not os.path.isfile(self.join(patch)):
762 if not os.path.isfile(self.join(patch)):
763 try:
763 try:
764 sno = int(patch)
764 sno = int(patch)
765 except(ValueError, OverflowError):
765 except(ValueError, OverflowError):
766 pass
766 pass
767 else:
767 else:
768 if sno < len(self.series):
768 if sno < len(self.series):
769 return self.series[sno]
769 return self.series[sno]
770 if not strict:
770 if not strict:
771 # return any partial match made above
771 # return any partial match made above
772 if res:
772 if res:
773 return res
773 return res
774 minus = patch.rfind('-')
774 minus = patch.rfind('-')
775 if minus >= 0:
775 if minus >= 0:
776 res = partial_name(patch[:minus])
776 res = partial_name(patch[:minus])
777 if res:
777 if res:
778 i = self.series.index(res)
778 i = self.series.index(res)
779 try:
779 try:
780 off = int(patch[minus+1:] or 1)
780 off = int(patch[minus+1:] or 1)
781 except(ValueError, OverflowError):
781 except(ValueError, OverflowError):
782 pass
782 pass
783 else:
783 else:
784 if i - off >= 0:
784 if i - off >= 0:
785 return self.series[i - off]
785 return self.series[i - off]
786 plus = patch.rfind('+')
786 plus = patch.rfind('+')
787 if plus >= 0:
787 if plus >= 0:
788 res = partial_name(patch[:plus])
788 res = partial_name(patch[:plus])
789 if res:
789 if res:
790 i = self.series.index(res)
790 i = self.series.index(res)
791 try:
791 try:
792 off = int(patch[plus+1:] or 1)
792 off = int(patch[plus+1:] or 1)
793 except(ValueError, OverflowError):
793 except(ValueError, OverflowError):
794 pass
794 pass
795 else:
795 else:
796 if i + off < len(self.series):
796 if i + off < len(self.series):
797 return self.series[i + off]
797 return self.series[i + off]
798 raise util.Abort(_("patch %s not in series") % patch)
798 raise util.Abort(_("patch %s not in series") % patch)
799
799
800 def push(self, repo, patch=None, force=False, list=False,
800 def push(self, repo, patch=None, force=False, list=False,
801 mergeq=None, wlock=None):
801 mergeq=None, wlock=None):
802 if not wlock:
802 if not wlock:
803 wlock = repo.wlock()
803 wlock = repo.wlock()
804 patch = self.lookup(patch)
804 patch = self.lookup(patch)
805 if patch and self.isapplied(patch):
805 if patch and self.isapplied(patch):
806 raise util.Abort(_("patch %s is already applied") % patch)
806 raise util.Abort(_("patch %s is already applied") % patch)
807 if self.series_end() == len(self.series):
807 if self.series_end() == len(self.series):
808 raise util.Abort(_("patch series fully applied"))
808 raise util.Abort(_("patch series fully applied"))
809 if not force:
809 if not force:
810 self.check_localchanges(repo)
810 self.check_localchanges(repo)
811
811
812 self.applied_dirty = 1;
812 self.applied_dirty = 1;
813 start = self.series_end()
813 start = self.series_end()
814 if start > 0:
814 if start > 0:
815 self.check_toppatch(repo)
815 self.check_toppatch(repo)
816 if not patch:
816 if not patch:
817 patch = self.series[start]
817 patch = self.series[start]
818 end = start + 1
818 end = start + 1
819 else:
819 else:
820 end = self.series.index(patch, start) + 1
820 end = self.series.index(patch, start) + 1
821 s = self.series[start:end]
821 s = self.series[start:end]
822 if mergeq:
822 if mergeq:
823 ret = self.mergepatch(repo, mergeq, s, wlock)
823 ret = self.mergepatch(repo, mergeq, s, wlock)
824 else:
824 else:
825 ret = self.apply(repo, s, list, wlock=wlock)
825 ret = self.apply(repo, s, list, wlock=wlock)
826 top = self.applied[-1].name
826 top = self.applied[-1].name
827 if ret[0]:
827 if ret[0]:
828 self.ui.write("Errors during apply, please fix and refresh %s\n" %
828 self.ui.write("Errors during apply, please fix and refresh %s\n" %
829 top)
829 top)
830 else:
830 else:
831 self.ui.write("Now at: %s\n" % top)
831 self.ui.write("Now at: %s\n" % top)
832 return ret[0]
832 return ret[0]
833
833
834 def pop(self, repo, patch=None, force=False, update=True, all=False,
834 def pop(self, repo, patch=None, force=False, update=True, all=False,
835 wlock=None):
835 wlock=None):
836 def getfile(f, rev):
836 def getfile(f, rev):
837 t = repo.file(f).read(rev)
837 t = repo.file(f).read(rev)
838 repo.wfile(f, "w").write(t)
838 repo.wfile(f, "w").write(t)
839
839
840 if not wlock:
840 if not wlock:
841 wlock = repo.wlock()
841 wlock = repo.wlock()
842 if patch:
842 if patch:
843 # index, rev, patch
843 # index, rev, patch
844 info = self.isapplied(patch)
844 info = self.isapplied(patch)
845 if not info:
845 if not info:
846 patch = self.lookup(patch)
846 patch = self.lookup(patch)
847 info = self.isapplied(patch)
847 info = self.isapplied(patch)
848 if not info:
848 if not info:
849 raise util.Abort(_("patch %s is not applied") % patch)
849 raise util.Abort(_("patch %s is not applied") % patch)
850 if len(self.applied) == 0:
850 if len(self.applied) == 0:
851 raise util.Abort(_("no patches applied"))
851 raise util.Abort(_("no patches applied"))
852
852
853 if not update:
853 if not update:
854 parents = repo.dirstate.parents()
854 parents = repo.dirstate.parents()
855 rr = [ revlog.bin(x.rev) for x in self.applied ]
855 rr = [ revlog.bin(x.rev) for x in self.applied ]
856 for p in parents:
856 for p in parents:
857 if p in rr:
857 if p in rr:
858 self.ui.warn("qpop: forcing dirstate update\n")
858 self.ui.warn("qpop: forcing dirstate update\n")
859 update = True
859 update = True
860
860
861 if not force and update:
861 if not force and update:
862 self.check_localchanges(repo)
862 self.check_localchanges(repo)
863
863
864 self.applied_dirty = 1;
864 self.applied_dirty = 1;
865 end = len(self.applied)
865 end = len(self.applied)
866 if not patch:
866 if not patch:
867 if all:
867 if all:
868 popi = 0
868 popi = 0
869 else:
869 else:
870 popi = len(self.applied) - 1
870 popi = len(self.applied) - 1
871 else:
871 else:
872 popi = info[0] + 1
872 popi = info[0] + 1
873 if popi >= end:
873 if popi >= end:
874 self.ui.warn("qpop: %s is already at the top\n" % patch)
874 self.ui.warn("qpop: %s is already at the top\n" % patch)
875 return
875 return
876 info = [ popi ] + [self.applied[popi].rev, self.applied[popi].name]
876 info = [ popi ] + [self.applied[popi].rev, self.applied[popi].name]
877
877
878 start = info[0]
878 start = info[0]
879 rev = revlog.bin(info[1])
879 rev = revlog.bin(info[1])
880
880
881 # we know there are no local changes, so we can make a simplified
881 # we know there are no local changes, so we can make a simplified
882 # form of hg.update.
882 # form of hg.update.
883 if update:
883 if update:
884 top = self.check_toppatch(repo)
884 top = self.check_toppatch(repo)
885 qp = self.qparents(repo, rev)
885 qp = self.qparents(repo, rev)
886 changes = repo.changelog.read(qp)
886 changes = repo.changelog.read(qp)
887 mmap = repo.manifest.read(changes[0])
887 mmap = repo.manifest.read(changes[0])
888 m, a, r, d, u = repo.status(qp, top)[:5]
888 m, a, r, d, u = repo.status(qp, top)[:5]
889 if d:
889 if d:
890 raise util.Abort("deletions found between repo revs")
890 raise util.Abort("deletions found between repo revs")
891 for f in m:
891 for f in m:
892 getfile(f, mmap[f])
892 getfile(f, mmap[f])
893 for f in r:
893 for f in r:
894 getfile(f, mmap[f])
894 getfile(f, mmap[f])
895 util.set_exec(repo.wjoin(f), mmap.execf(f))
895 util.set_exec(repo.wjoin(f), mmap.execf(f))
896 repo.dirstate.update(m + r, 'n')
896 repo.dirstate.update(m + r, 'n')
897 for f in a:
897 for f in a:
898 try:
898 try:
899 os.unlink(repo.wjoin(f))
899 os.unlink(repo.wjoin(f))
900 except OSError, e:
900 except OSError, e:
901 if e.errno != errno.ENOENT:
901 if e.errno != errno.ENOENT:
902 raise
902 raise
903 try: os.removedirs(os.path.dirname(repo.wjoin(f)))
903 try: os.removedirs(os.path.dirname(repo.wjoin(f)))
904 except: pass
904 except: pass
905 if a:
905 if a:
906 repo.dirstate.forget(a)
906 repo.dirstate.forget(a)
907 repo.dirstate.setparents(qp, revlog.nullid)
907 repo.dirstate.setparents(qp, revlog.nullid)
908 self.strip(repo, rev, update=False, backup='strip', wlock=wlock)
908 self.strip(repo, rev, update=False, backup='strip', wlock=wlock)
909 del self.applied[start:end]
909 del self.applied[start:end]
910 if len(self.applied):
910 if len(self.applied):
911 self.ui.write("Now at: %s\n" % self.applied[-1].name)
911 self.ui.write("Now at: %s\n" % self.applied[-1].name)
912 else:
912 else:
913 self.ui.write("Patch queue now empty\n")
913 self.ui.write("Patch queue now empty\n")
914
914
915 def diff(self, repo, pats, opts):
915 def diff(self, repo, pats, opts):
916 top = self.check_toppatch(repo)
916 top = self.check_toppatch(repo)
917 if not top:
917 if not top:
918 self.ui.write("No patches applied\n")
918 self.ui.write("No patches applied\n")
919 return
919 return
920 qp = self.qparents(repo, top)
920 qp = self.qparents(repo, top)
921 if opts.get('git'):
921 if opts.get('git'):
922 self.diffopts().git = True
922 self.diffopts().git = True
923 self.printdiff(repo, qp, files=pats, opts=opts)
923 self.printdiff(repo, qp, files=pats, opts=opts)
924
924
925 def refresh(self, repo, pats=None, **opts):
925 def refresh(self, repo, pats=None, **opts):
926 if len(self.applied) == 0:
926 if len(self.applied) == 0:
927 self.ui.write("No patches applied\n")
927 self.ui.write("No patches applied\n")
928 return 1
928 return 1
929 wlock = repo.wlock()
929 wlock = repo.wlock()
930 self.check_toppatch(repo)
930 self.check_toppatch(repo)
931 (top, patchfn) = (self.applied[-1].rev, self.applied[-1].name)
931 (top, patchfn) = (self.applied[-1].rev, self.applied[-1].name)
932 top = revlog.bin(top)
932 top = revlog.bin(top)
933 cparents = repo.changelog.parents(top)
933 cparents = repo.changelog.parents(top)
934 patchparent = self.qparents(repo, top)
934 patchparent = self.qparents(repo, top)
935 message, comments, user, date, patchfound = self.readheaders(patchfn)
935 message, comments, user, date, patchfound = self.readheaders(patchfn)
936
936
937 patchf = self.opener(patchfn, "w")
937 patchf = self.opener(patchfn, "w")
938 msg = opts.get('msg', '').rstrip()
938 msg = opts.get('msg', '').rstrip()
939 if msg:
939 if msg:
940 if comments:
940 if comments:
941 # Remove existing message.
941 # Remove existing message.
942 ci = 0
942 ci = 0
943 for mi in xrange(len(message)):
943 for mi in xrange(len(message)):
944 while message[mi] != comments[ci]:
944 while message[mi] != comments[ci]:
945 ci += 1
945 ci += 1
946 del comments[ci]
946 del comments[ci]
947 comments.append(msg)
947 comments.append(msg)
948 if comments:
948 if comments:
949 comments = "\n".join(comments) + '\n\n'
949 comments = "\n".join(comments) + '\n\n'
950 patchf.write(comments)
950 patchf.write(comments)
951
951
952 if opts.get('git'):
952 if opts.get('git'):
953 self.diffopts().git = True
953 self.diffopts().git = True
954 fns, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
954 fns, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
955 tip = repo.changelog.tip()
955 tip = repo.changelog.tip()
956 if top == tip:
956 if top == tip:
957 # if the top of our patch queue is also the tip, there is an
957 # if the top of our patch queue is also the tip, there is an
958 # optimization here. We update the dirstate in place and strip
958 # optimization here. We update the dirstate in place and strip
959 # off the tip commit. Then just commit the current directory
959 # off the tip commit. Then just commit the current directory
960 # tree. We can also send repo.commit the list of files
960 # tree. We can also send repo.commit the list of files
961 # changed to speed up the diff
961 # changed to speed up the diff
962 #
962 #
963 # in short mode, we only diff the files included in the
963 # in short mode, we only diff the files included in the
964 # patch already
964 # patch already
965 #
965 #
966 # this should really read:
966 # this should really read:
967 # mm, dd, aa, aa2, uu = repo.status(tip, patchparent)[:5]
967 # mm, dd, aa, aa2, uu = repo.status(tip, patchparent)[:5]
968 # but we do it backwards to take advantage of manifest/chlog
968 # but we do it backwards to take advantage of manifest/chlog
969 # caching against the next repo.status call
969 # caching against the next repo.status call
970 #
970 #
971 mm, aa, dd, aa2, uu = repo.status(patchparent, tip)[:5]
971 mm, aa, dd, aa2, uu = repo.status(patchparent, tip)[:5]
972 changes = repo.changelog.read(tip)
972 changes = repo.changelog.read(tip)
973 man = repo.manifest.read(changes[0])
973 man = repo.manifest.read(changes[0])
974 aaa = aa[:]
974 aaa = aa[:]
975 if opts.get('short'):
975 if opts.get('short'):
976 filelist = mm + aa + dd
976 filelist = mm + aa + dd
977 else:
977 else:
978 filelist = None
978 filelist = None
979 m, a, r, d, u = repo.status(files=filelist)[:5]
979 m, a, r, d, u = repo.status(files=filelist)[:5]
980
980
981 # we might end up with files that were added between tip and
981 # we might end up with files that were added between tip and
982 # the dirstate parent, but then changed in the local dirstate.
982 # the dirstate parent, but then changed in the local dirstate.
983 # in this case, we want them to only show up in the added section
983 # in this case, we want them to only show up in the added section
984 for x in m:
984 for x in m:
985 if x not in aa:
985 if x not in aa:
986 mm.append(x)
986 mm.append(x)
987 # we might end up with files added by the local dirstate that
987 # we might end up with files added by the local dirstate that
988 # were deleted by the patch. In this case, they should only
988 # were deleted by the patch. In this case, they should only
989 # show up in the changed section.
989 # show up in the changed section.
990 for x in a:
990 for x in a:
991 if x in dd:
991 if x in dd:
992 del dd[dd.index(x)]
992 del dd[dd.index(x)]
993 mm.append(x)
993 mm.append(x)
994 else:
994 else:
995 aa.append(x)
995 aa.append(x)
996 # make sure any files deleted in the local dirstate
996 # make sure any files deleted in the local dirstate
997 # are not in the add or change column of the patch
997 # are not in the add or change column of the patch
998 forget = []
998 forget = []
999 for x in d + r:
999 for x in d + r:
1000 if x in aa:
1000 if x in aa:
1001 del aa[aa.index(x)]
1001 del aa[aa.index(x)]
1002 forget.append(x)
1002 forget.append(x)
1003 continue
1003 continue
1004 elif x in mm:
1004 elif x in mm:
1005 del mm[mm.index(x)]
1005 del mm[mm.index(x)]
1006 dd.append(x)
1006 dd.append(x)
1007
1007
1008 m = util.unique(mm)
1008 m = util.unique(mm)
1009 r = util.unique(dd)
1009 r = util.unique(dd)
1010 a = util.unique(aa)
1010 a = util.unique(aa)
1011 filelist = filter(matchfn, util.unique(m + r + a))
1011 filelist = filter(matchfn, util.unique(m + r + a))
1012 patch.diff(repo, patchparent, files=filelist, match=matchfn,
1012 patch.diff(repo, patchparent, files=filelist, match=matchfn,
1013 fp=patchf, changes=(m, a, r, [], u),
1013 fp=patchf, changes=(m, a, r, [], u),
1014 opts=self.diffopts())
1014 opts=self.diffopts())
1015 patchf.close()
1015 patchf.close()
1016
1016
1017 repo.dirstate.setparents(*cparents)
1017 repo.dirstate.setparents(*cparents)
1018 copies = {}
1018 copies = {}
1019 for dst in a:
1019 for dst in a:
1020 src = repo.dirstate.copied(dst)
1020 src = repo.dirstate.copied(dst)
1021 if src is None:
1021 if src is None:
1022 continue
1022 continue
1023 copies.setdefault(src, []).append(dst)
1023 copies.setdefault(src, []).append(dst)
1024 repo.dirstate.update(a, 'a')
1024 repo.dirstate.update(a, 'a')
1025 # remember the copies between patchparent and tip
1025 # remember the copies between patchparent and tip
1026 # this may be slow, so don't do it if we're not tracking copies
1026 # this may be slow, so don't do it if we're not tracking copies
1027 if self.diffopts().git:
1027 if self.diffopts().git:
1028 for dst in aaa:
1028 for dst in aaa:
1029 f = repo.file(dst)
1029 f = repo.file(dst)
1030 src = f.renamed(man[dst])
1030 src = f.renamed(man[dst])
1031 if src:
1031 if src:
1032 copies[src[0]] = copies.get(dst, [])
1032 copies[src[0]] = copies.get(dst, [])
1033 if dst in a:
1033 if dst in a:
1034 copies[src[0]].append(dst)
1034 copies[src[0]].append(dst)
1035 # we can't copy a file created by the patch itself
1035 # we can't copy a file created by the patch itself
1036 if dst in copies:
1036 if dst in copies:
1037 del copies[dst]
1037 del copies[dst]
1038 for src, dsts in copies.iteritems():
1038 for src, dsts in copies.iteritems():
1039 for dst in dsts:
1039 for dst in dsts:
1040 repo.dirstate.copy(src, dst)
1040 repo.dirstate.copy(src, dst)
1041 repo.dirstate.update(r, 'r')
1041 repo.dirstate.update(r, 'r')
1042 # if the patch excludes a modified file, mark that file with mtime=0
1042 # if the patch excludes a modified file, mark that file with mtime=0
1043 # so status can see it.
1043 # so status can see it.
1044 mm = []
1044 mm = []
1045 for i in xrange(len(m)-1, -1, -1):
1045 for i in xrange(len(m)-1, -1, -1):
1046 if not matchfn(m[i]):
1046 if not matchfn(m[i]):
1047 mm.append(m[i])
1047 mm.append(m[i])
1048 del m[i]
1048 del m[i]
1049 repo.dirstate.update(m, 'n')
1049 repo.dirstate.update(m, 'n')
1050 repo.dirstate.update(mm, 'n', st_mtime=0)
1050 repo.dirstate.update(mm, 'n', st_mtime=0)
1051 repo.dirstate.forget(forget)
1051 repo.dirstate.forget(forget)
1052
1052
1053 if not msg:
1053 if not msg:
1054 if not message:
1054 if not message:
1055 message = "patch queue: %s\n" % patchfn
1055 message = "patch queue: %s\n" % patchfn
1056 else:
1056 else:
1057 message = "\n".join(message)
1057 message = "\n".join(message)
1058 else:
1058 else:
1059 message = msg
1059 message = msg
1060
1060
1061 self.strip(repo, top, update=False, backup='strip', wlock=wlock)
1061 self.strip(repo, top, update=False, backup='strip', wlock=wlock)
1062 n = repo.commit(filelist, message, changes[1], force=1, wlock=wlock)
1062 n = repo.commit(filelist, message, changes[1], force=1, wlock=wlock)
1063 self.applied[-1] = statusentry(revlog.hex(n), patchfn)
1063 self.applied[-1] = statusentry(revlog.hex(n), patchfn)
1064 self.applied_dirty = 1
1064 self.applied_dirty = 1
1065 else:
1065 else:
1066 self.printdiff(repo, patchparent, fp=patchf)
1066 self.printdiff(repo, patchparent, fp=patchf)
1067 patchf.close()
1067 patchf.close()
1068 added = repo.status()[1]
1068 added = repo.status()[1]
1069 for a in added:
1069 for a in added:
1070 f = repo.wjoin(a)
1070 f = repo.wjoin(a)
1071 try:
1071 try:
1072 os.unlink(f)
1072 os.unlink(f)
1073 except OSError, e:
1073 except OSError, e:
1074 if e.errno != errno.ENOENT:
1074 if e.errno != errno.ENOENT:
1075 raise
1075 raise
1076 try: os.removedirs(os.path.dirname(f))
1076 try: os.removedirs(os.path.dirname(f))
1077 except: pass
1077 except: pass
1078 # forget the file copies in the dirstate
1078 # forget the file copies in the dirstate
1079 # push should readd the files later on
1079 # push should readd the files later on
1080 repo.dirstate.forget(added)
1080 repo.dirstate.forget(added)
1081 self.pop(repo, force=True, wlock=wlock)
1081 self.pop(repo, force=True, wlock=wlock)
1082 self.push(repo, force=True, wlock=wlock)
1082 self.push(repo, force=True, wlock=wlock)
1083
1083
1084 def init(self, repo, create=False):
1084 def init(self, repo, create=False):
1085 if not create and os.path.isdir(self.path):
1085 if not create and os.path.isdir(self.path):
1086 raise util.Abort(_("patch queue directory already exists"))
1086 raise util.Abort(_("patch queue directory already exists"))
1087 try:
1087 try:
1088 os.mkdir(self.path)
1088 os.mkdir(self.path)
1089 except OSError, inst:
1089 except OSError, inst:
1090 if inst.errno != errno.EEXIST or not create:
1090 if inst.errno != errno.EEXIST or not create:
1091 raise
1091 raise
1092 if create:
1092 if create:
1093 return self.qrepo(create=True)
1093 return self.qrepo(create=True)
1094
1094
1095 def unapplied(self, repo, patch=None):
1095 def unapplied(self, repo, patch=None):
1096 if patch and patch not in self.series:
1096 if patch and patch not in self.series:
1097 raise util.Abort(_("patch %s is not in series file") % patch)
1097 raise util.Abort(_("patch %s is not in series file") % patch)
1098 if not patch:
1098 if not patch:
1099 start = self.series_end()
1099 start = self.series_end()
1100 else:
1100 else:
1101 start = self.series.index(patch) + 1
1101 start = self.series.index(patch) + 1
1102 unapplied = []
1102 unapplied = []
1103 for i in xrange(start, len(self.series)):
1103 for i in xrange(start, len(self.series)):
1104 pushable, reason = self.pushable(i)
1104 pushable, reason = self.pushable(i)
1105 if pushable:
1105 if pushable:
1106 unapplied.append((i, self.series[i]))
1106 unapplied.append((i, self.series[i]))
1107 self.explain_pushable(i)
1107 self.explain_pushable(i)
1108 return unapplied
1108 return unapplied
1109
1109
1110 def qseries(self, repo, missing=None, start=0, length=0, status=None,
1110 def qseries(self, repo, missing=None, start=0, length=0, status=None,
1111 summary=False):
1111 summary=False):
1112 def displayname(patchname):
1112 def displayname(patchname):
1113 if summary:
1113 if summary:
1114 msg = self.readheaders(patchname)[0]
1114 msg = self.readheaders(patchname)[0]
1115 msg = msg and ': ' + msg[0] or ': '
1115 msg = msg and ': ' + msg[0] or ': '
1116 else:
1116 else:
1117 msg = ''
1117 msg = ''
1118 return '%s%s' % (patchname, msg)
1118 return '%s%s' % (patchname, msg)
1119
1119
1120 def pname(i):
1120 def pname(i):
1121 if status == 'A':
1121 if status == 'A':
1122 return self.applied[i].name
1122 return self.applied[i].name
1123 else:
1123 else:
1124 return self.series[i]
1124 return self.series[i]
1125
1125
1126 applied = dict.fromkeys([p.name for p in self.applied])
1126 applied = dict.fromkeys([p.name for p in self.applied])
1127 if not length:
1127 if not length:
1128 length = len(self.series) - start
1128 length = len(self.series) - start
1129 if not missing:
1129 if not missing:
1130 for i in xrange(start, start+length):
1130 for i in xrange(start, start+length):
1131 pfx = ''
1131 pfx = ''
1132 patch = pname(i)
1132 patch = pname(i)
1133 if self.ui.verbose:
1133 if self.ui.verbose:
1134 if patch in applied:
1134 if patch in applied:
1135 stat = 'A'
1135 stat = 'A'
1136 elif self.pushable(i)[0]:
1136 elif self.pushable(i)[0]:
1137 stat = 'U'
1137 stat = 'U'
1138 else:
1138 else:
1139 stat = 'G'
1139 stat = 'G'
1140 pfx = '%d %s ' % (i, stat)
1140 pfx = '%d %s ' % (i, stat)
1141 self.ui.write('%s%s\n' % (pfx, displayname(patch)))
1141 self.ui.write('%s%s\n' % (pfx, displayname(patch)))
1142 else:
1142 else:
1143 msng_list = []
1143 msng_list = []
1144 for root, dirs, files in os.walk(self.path):
1144 for root, dirs, files in os.walk(self.path):
1145 d = root[len(self.path) + 1:]
1145 d = root[len(self.path) + 1:]
1146 for f in files:
1146 for f in files:
1147 fl = os.path.join(d, f)
1147 fl = os.path.join(d, f)
1148 if (fl not in self.series and
1148 if (fl not in self.series and
1149 fl not in (self.status_path, self.series_path)
1149 fl not in (self.status_path, self.series_path)
1150 and not fl.startswith('.')):
1150 and not fl.startswith('.')):
1151 msng_list.append(fl)
1151 msng_list.append(fl)
1152 msng_list.sort()
1152 msng_list.sort()
1153 for x in msng_list:
1153 for x in msng_list:
1154 pfx = self.ui.verbose and ('D ') or ''
1154 pfx = self.ui.verbose and ('D ') or ''
1155 self.ui.write("%s%s\n" % (pfx, displayname(x)))
1155 self.ui.write("%s%s\n" % (pfx, displayname(x)))
1156
1156
1157 def issaveline(self, l):
1157 def issaveline(self, l):
1158 if l.name == '.hg.patches.save.line':
1158 if l.name == '.hg.patches.save.line':
1159 return True
1159 return True
1160
1160
1161 def qrepo(self, create=False):
1161 def qrepo(self, create=False):
1162 if create or os.path.isdir(self.join(".hg")):
1162 if create or os.path.isdir(self.join(".hg")):
1163 return hg.repository(self.ui, path=self.path, create=create)
1163 return hg.repository(self.ui, path=self.path, create=create)
1164
1164
1165 def restore(self, repo, rev, delete=None, qupdate=None):
1165 def restore(self, repo, rev, delete=None, qupdate=None):
1166 c = repo.changelog.read(rev)
1166 c = repo.changelog.read(rev)
1167 desc = c[4].strip()
1167 desc = c[4].strip()
1168 lines = desc.splitlines()
1168 lines = desc.splitlines()
1169 i = 0
1169 i = 0
1170 datastart = None
1170 datastart = None
1171 series = []
1171 series = []
1172 applied = []
1172 applied = []
1173 qpp = None
1173 qpp = None
1174 for i in xrange(0, len(lines)):
1174 for i in xrange(0, len(lines)):
1175 if lines[i] == 'Patch Data:':
1175 if lines[i] == 'Patch Data:':
1176 datastart = i + 1
1176 datastart = i + 1
1177 elif lines[i].startswith('Dirstate:'):
1177 elif lines[i].startswith('Dirstate:'):
1178 l = lines[i].rstrip()
1178 l = lines[i].rstrip()
1179 l = l[10:].split(' ')
1179 l = l[10:].split(' ')
1180 qpp = [ hg.bin(x) for x in l ]
1180 qpp = [ hg.bin(x) for x in l ]
1181 elif datastart != None:
1181 elif datastart != None:
1182 l = lines[i].rstrip()
1182 l = lines[i].rstrip()
1183 se = statusentry(l)
1183 se = statusentry(l)
1184 file_ = se.name
1184 file_ = se.name
1185 if se.rev:
1185 if se.rev:
1186 applied.append(se)
1186 applied.append(se)
1187 else:
1187 else:
1188 series.append(file_)
1188 series.append(file_)
1189 if datastart == None:
1189 if datastart == None:
1190 self.ui.warn("No saved patch data found\n")
1190 self.ui.warn("No saved patch data found\n")
1191 return 1
1191 return 1
1192 self.ui.warn("restoring status: %s\n" % lines[0])
1192 self.ui.warn("restoring status: %s\n" % lines[0])
1193 self.full_series = series
1193 self.full_series = series
1194 self.applied = applied
1194 self.applied = applied
1195 self.parse_series()
1195 self.parse_series()
1196 self.series_dirty = 1
1196 self.series_dirty = 1
1197 self.applied_dirty = 1
1197 self.applied_dirty = 1
1198 heads = repo.changelog.heads()
1198 heads = repo.changelog.heads()
1199 if delete:
1199 if delete:
1200 if rev not in heads:
1200 if rev not in heads:
1201 self.ui.warn("save entry has children, leaving it alone\n")
1201 self.ui.warn("save entry has children, leaving it alone\n")
1202 else:
1202 else:
1203 self.ui.warn("removing save entry %s\n" % hg.short(rev))
1203 self.ui.warn("removing save entry %s\n" % hg.short(rev))
1204 pp = repo.dirstate.parents()
1204 pp = repo.dirstate.parents()
1205 if rev in pp:
1205 if rev in pp:
1206 update = True
1206 update = True
1207 else:
1207 else:
1208 update = False
1208 update = False
1209 self.strip(repo, rev, update=update, backup='strip')
1209 self.strip(repo, rev, update=update, backup='strip')
1210 if qpp:
1210 if qpp:
1211 self.ui.warn("saved queue repository parents: %s %s\n" %
1211 self.ui.warn("saved queue repository parents: %s %s\n" %
1212 (hg.short(qpp[0]), hg.short(qpp[1])))
1212 (hg.short(qpp[0]), hg.short(qpp[1])))
1213 if qupdate:
1213 if qupdate:
1214 print "queue directory updating"
1214 print "queue directory updating"
1215 r = self.qrepo()
1215 r = self.qrepo()
1216 if not r:
1216 if not r:
1217 self.ui.warn("Unable to load queue repository\n")
1217 self.ui.warn("Unable to load queue repository\n")
1218 return 1
1218 return 1
1219 hg.clean(r, qpp[0])
1219 hg.clean(r, qpp[0])
1220
1220
1221 def save(self, repo, msg=None):
1221 def save(self, repo, msg=None):
1222 if len(self.applied) == 0:
1222 if len(self.applied) == 0:
1223 self.ui.warn("save: no patches applied, exiting\n")
1223 self.ui.warn("save: no patches applied, exiting\n")
1224 return 1
1224 return 1
1225 if self.issaveline(self.applied[-1]):
1225 if self.issaveline(self.applied[-1]):
1226 self.ui.warn("status is already saved\n")
1226 self.ui.warn("status is already saved\n")
1227 return 1
1227 return 1
1228
1228
1229 ar = [ ':' + x for x in self.full_series ]
1229 ar = [ ':' + x for x in self.full_series ]
1230 if not msg:
1230 if not msg:
1231 msg = "hg patches saved state"
1231 msg = "hg patches saved state"
1232 else:
1232 else:
1233 msg = "hg patches: " + msg.rstrip('\r\n')
1233 msg = "hg patches: " + msg.rstrip('\r\n')
1234 r = self.qrepo()
1234 r = self.qrepo()
1235 if r:
1235 if r:
1236 pp = r.dirstate.parents()
1236 pp = r.dirstate.parents()
1237 msg += "\nDirstate: %s %s" % (hg.hex(pp[0]), hg.hex(pp[1]))
1237 msg += "\nDirstate: %s %s" % (hg.hex(pp[0]), hg.hex(pp[1]))
1238 msg += "\n\nPatch Data:\n"
1238 msg += "\n\nPatch Data:\n"
1239 text = msg + "\n".join([str(x) for x in self.applied]) + '\n' + (ar and
1239 text = msg + "\n".join([str(x) for x in self.applied]) + '\n' + (ar and
1240 "\n".join(ar) + '\n' or "")
1240 "\n".join(ar) + '\n' or "")
1241 n = repo.commit(None, text, user=None, force=1)
1241 n = repo.commit(None, text, user=None, force=1)
1242 if not n:
1242 if not n:
1243 self.ui.warn("repo commit failed\n")
1243 self.ui.warn("repo commit failed\n")
1244 return 1
1244 return 1
1245 self.applied.append(statusentry(revlog.hex(n),'.hg.patches.save.line'))
1245 self.applied.append(statusentry(revlog.hex(n),'.hg.patches.save.line'))
1246 self.applied_dirty = 1
1246 self.applied_dirty = 1
1247
1247
1248 def full_series_end(self):
1248 def full_series_end(self):
1249 if len(self.applied) > 0:
1249 if len(self.applied) > 0:
1250 p = self.applied[-1].name
1250 p = self.applied[-1].name
1251 end = self.find_series(p)
1251 end = self.find_series(p)
1252 if end == None:
1252 if end == None:
1253 return len(self.full_series)
1253 return len(self.full_series)
1254 return end + 1
1254 return end + 1
1255 return 0
1255 return 0
1256
1256
1257 def series_end(self, all_patches=False):
1257 def series_end(self, all_patches=False):
1258 end = 0
1258 end = 0
1259 def next(start):
1259 def next(start):
1260 if all_patches:
1260 if all_patches:
1261 return start
1261 return start
1262 i = start
1262 i = start
1263 while i < len(self.series):
1263 while i < len(self.series):
1264 p, reason = self.pushable(i)
1264 p, reason = self.pushable(i)
1265 if p:
1265 if p:
1266 break
1266 break
1267 self.explain_pushable(i)
1267 self.explain_pushable(i)
1268 i += 1
1268 i += 1
1269 return i
1269 return i
1270 if len(self.applied) > 0:
1270 if len(self.applied) > 0:
1271 p = self.applied[-1].name
1271 p = self.applied[-1].name
1272 try:
1272 try:
1273 end = self.series.index(p)
1273 end = self.series.index(p)
1274 except ValueError:
1274 except ValueError:
1275 return 0
1275 return 0
1276 return next(end + 1)
1276 return next(end + 1)
1277 return next(end)
1277 return next(end)
1278
1278
1279 def appliedname(self, index):
1279 def appliedname(self, index):
1280 pname = self.applied[index].name
1280 pname = self.applied[index].name
1281 if not self.ui.verbose:
1281 if not self.ui.verbose:
1282 p = pname
1282 p = pname
1283 else:
1283 else:
1284 p = str(self.series.index(pname)) + " " + pname
1284 p = str(self.series.index(pname)) + " " + pname
1285 return p
1285 return p
1286
1286
1287 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1287 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1288 force=None, git=False):
1288 force=None, git=False):
1289 def checkseries(patchname):
1289 def checkseries(patchname):
1290 if patchname in self.series:
1290 if patchname in self.series:
1291 raise util.Abort(_('patch %s is already in the series file')
1291 raise util.Abort(_('patch %s is already in the series file')
1292 % patchname)
1292 % patchname)
1293 def checkfile(patchname):
1293 def checkfile(patchname):
1294 if not force and os.path.exists(self.join(patchname)):
1294 if not force and os.path.exists(self.join(patchname)):
1295 raise util.Abort(_('patch "%s" already exists')
1295 raise util.Abort(_('patch "%s" already exists')
1296 % patchname)
1296 % patchname)
1297
1297
1298 if rev:
1298 if rev:
1299 if files:
1299 if files:
1300 raise util.Abort(_('option "-r" not valid when importing '
1300 raise util.Abort(_('option "-r" not valid when importing '
1301 'files'))
1301 'files'))
1302 rev = cmdutil.revrange(repo, rev)
1302 rev = cmdutil.revrange(repo, rev)
1303 rev.sort(lambda x, y: cmp(y, x))
1303 rev.sort(lambda x, y: cmp(y, x))
1304 if (len(files) > 1 or len(rev) > 1) and patchname:
1304 if (len(files) > 1 or len(rev) > 1) and patchname:
1305 raise util.Abort(_('option "-n" not valid when importing multiple '
1305 raise util.Abort(_('option "-n" not valid when importing multiple '
1306 'patches'))
1306 'patches'))
1307 i = 0
1307 i = 0
1308 added = []
1308 added = []
1309 if rev:
1309 if rev:
1310 # If mq patches are applied, we can only import revisions
1310 # If mq patches are applied, we can only import revisions
1311 # that form a linear path to qbase.
1311 # that form a linear path to qbase.
1312 # Otherwise, they should form a linear path to a head.
1312 # Otherwise, they should form a linear path to a head.
1313 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1313 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1314 if len(heads) > 1:
1314 if len(heads) > 1:
1315 raise util.Abort(_('revision %d is the root of more than one '
1315 raise util.Abort(_('revision %d is the root of more than one '
1316 'branch') % rev[-1])
1316 'branch') % rev[-1])
1317 if self.applied:
1317 if self.applied:
1318 base = revlog.hex(repo.changelog.node(rev[0]))
1318 base = revlog.hex(repo.changelog.node(rev[0]))
1319 if base in [n.rev for n in self.applied]:
1319 if base in [n.rev for n in self.applied]:
1320 raise util.Abort(_('revision %d is already managed')
1320 raise util.Abort(_('revision %d is already managed')
1321 % rev[0])
1321 % rev[0])
1322 if heads != [revlog.bin(self.applied[-1].rev)]:
1322 if heads != [revlog.bin(self.applied[-1].rev)]:
1323 raise util.Abort(_('revision %d is not the parent of '
1323 raise util.Abort(_('revision %d is not the parent of '
1324 'the queue') % rev[0])
1324 'the queue') % rev[0])
1325 base = repo.changelog.rev(revlog.bin(self.applied[0].rev))
1325 base = repo.changelog.rev(revlog.bin(self.applied[0].rev))
1326 lastparent = repo.changelog.parentrevs(base)[0]
1326 lastparent = repo.changelog.parentrevs(base)[0]
1327 else:
1327 else:
1328 if heads != [repo.changelog.node(rev[0])]:
1328 if heads != [repo.changelog.node(rev[0])]:
1329 raise util.Abort(_('revision %d has unmanaged children')
1329 raise util.Abort(_('revision %d has unmanaged children')
1330 % rev[0])
1330 % rev[0])
1331 lastparent = None
1331 lastparent = None
1332
1332
1333 if git:
1333 if git:
1334 self.diffopts().git = True
1334 self.diffopts().git = True
1335
1335
1336 for r in rev:
1336 for r in rev:
1337 p1, p2 = repo.changelog.parentrevs(r)
1337 p1, p2 = repo.changelog.parentrevs(r)
1338 n = repo.changelog.node(r)
1338 n = repo.changelog.node(r)
1339 if p2 != revlog.nullrev:
1339 if p2 != revlog.nullrev:
1340 raise util.Abort(_('cannot import merge revision %d') % r)
1340 raise util.Abort(_('cannot import merge revision %d') % r)
1341 if lastparent and lastparent != r:
1341 if lastparent and lastparent != r:
1342 raise util.Abort(_('revision %d is not the parent of %d')
1342 raise util.Abort(_('revision %d is not the parent of %d')
1343 % (r, lastparent))
1343 % (r, lastparent))
1344 lastparent = p1
1344 lastparent = p1
1345
1345
1346 if not patchname:
1346 if not patchname:
1347 patchname = normname('%d.diff' % r)
1347 patchname = normname('%d.diff' % r)
1348 checkseries(patchname)
1348 checkseries(patchname)
1349 checkfile(patchname)
1349 checkfile(patchname)
1350 self.full_series.insert(0, patchname)
1350 self.full_series.insert(0, patchname)
1351
1351
1352 patchf = self.opener(patchname, "w")
1352 patchf = self.opener(patchname, "w")
1353 patch.export(repo, [n], fp=patchf, opts=self.diffopts())
1353 patch.export(repo, [n], fp=patchf, opts=self.diffopts())
1354 patchf.close()
1354 patchf.close()
1355
1355
1356 se = statusentry(revlog.hex(n), patchname)
1356 se = statusentry(revlog.hex(n), patchname)
1357 self.applied.insert(0, se)
1357 self.applied.insert(0, se)
1358
1358
1359 added.append(patchname)
1359 added.append(patchname)
1360 patchname = None
1360 patchname = None
1361 self.parse_series()
1361 self.parse_series()
1362 self.applied_dirty = 1
1362 self.applied_dirty = 1
1363
1363
1364 for filename in files:
1364 for filename in files:
1365 if existing:
1365 if existing:
1366 if filename == '-':
1366 if filename == '-':
1367 raise util.Abort(_('-e is incompatible with import from -'))
1367 raise util.Abort(_('-e is incompatible with import from -'))
1368 if not patchname:
1368 if not patchname:
1369 patchname = normname(filename)
1369 patchname = normname(filename)
1370 if not os.path.isfile(self.join(patchname)):
1370 if not os.path.isfile(self.join(patchname)):
1371 raise util.Abort(_("patch %s does not exist") % patchname)
1371 raise util.Abort(_("patch %s does not exist") % patchname)
1372 else:
1372 else:
1373 try:
1373 try:
1374 if filename == '-':
1374 if filename == '-':
1375 if not patchname:
1375 if not patchname:
1376 raise util.Abort(_('need --name to import a patch from -'))
1376 raise util.Abort(_('need --name to import a patch from -'))
1377 text = sys.stdin.read()
1377 text = sys.stdin.read()
1378 else:
1378 else:
1379 text = file(filename).read()
1379 text = file(filename).read()
1380 except IOError:
1380 except IOError:
1381 raise util.Abort(_("unable to read %s") % patchname)
1381 raise util.Abort(_("unable to read %s") % patchname)
1382 if not patchname:
1382 if not patchname:
1383 patchname = normname(os.path.basename(filename))
1383 patchname = normname(os.path.basename(filename))
1384 checkfile(patchname)
1384 checkfile(patchname)
1385 patchf = self.opener(patchname, "w")
1385 patchf = self.opener(patchname, "w")
1386 patchf.write(text)
1386 patchf.write(text)
1387 checkseries(patchname)
1387 checkseries(patchname)
1388 index = self.full_series_end() + i
1388 index = self.full_series_end() + i
1389 self.full_series[index:index] = [patchname]
1389 self.full_series[index:index] = [patchname]
1390 self.parse_series()
1390 self.parse_series()
1391 self.ui.warn("adding %s to series file\n" % patchname)
1391 self.ui.warn("adding %s to series file\n" % patchname)
1392 i += 1
1392 i += 1
1393 added.append(patchname)
1393 added.append(patchname)
1394 patchname = None
1394 patchname = None
1395 self.series_dirty = 1
1395 self.series_dirty = 1
1396 qrepo = self.qrepo()
1396 qrepo = self.qrepo()
1397 if qrepo:
1397 if qrepo:
1398 qrepo.add(added)
1398 qrepo.add(added)
1399
1399
1400 def delete(ui, repo, *patches, **opts):
1400 def delete(ui, repo, *patches, **opts):
1401 """remove patches from queue
1401 """remove patches from queue
1402
1402
1403 With --rev, mq will stop managing the named revisions. The
1403 With --rev, mq will stop managing the named revisions. The
1404 patches must be applied and at the base of the stack. This option
1404 patches must be applied and at the base of the stack. This option
1405 is useful when the patches have been applied upstream.
1405 is useful when the patches have been applied upstream.
1406
1406
1407 Otherwise, the patches must not be applied.
1407 Otherwise, the patches must not be applied.
1408
1408
1409 With --keep, the patch files are preserved in the patch directory."""
1409 With --keep, the patch files are preserved in the patch directory."""
1410 q = repo.mq
1410 q = repo.mq
1411 q.delete(repo, patches, opts)
1411 q.delete(repo, patches, opts)
1412 q.save_dirty()
1412 q.save_dirty()
1413 return 0
1413 return 0
1414
1414
1415 def applied(ui, repo, patch=None, **opts):
1415 def applied(ui, repo, patch=None, **opts):
1416 """print the patches already applied"""
1416 """print the patches already applied"""
1417 q = repo.mq
1417 q = repo.mq
1418 if patch:
1418 if patch:
1419 if patch not in q.series:
1419 if patch not in q.series:
1420 raise util.Abort(_("patch %s is not in series file") % patch)
1420 raise util.Abort(_("patch %s is not in series file") % patch)
1421 end = q.series.index(patch) + 1
1421 end = q.series.index(patch) + 1
1422 else:
1422 else:
1423 end = len(q.applied)
1423 end = len(q.applied)
1424 if not end:
1424 if not end:
1425 return
1425 return
1426
1426
1427 return q.qseries(repo, length=end, status='A', summary=opts.get('summary'))
1427 return q.qseries(repo, length=end, status='A', summary=opts.get('summary'))
1428
1428
1429 def unapplied(ui, repo, patch=None, **opts):
1429 def unapplied(ui, repo, patch=None, **opts):
1430 """print the patches not yet applied"""
1430 """print the patches not yet applied"""
1431 q = repo.mq
1431 q = repo.mq
1432 if patch:
1432 if patch:
1433 if patch not in q.series:
1433 if patch not in q.series:
1434 raise util.Abort(_("patch %s is not in series file") % patch)
1434 raise util.Abort(_("patch %s is not in series file") % patch)
1435 start = q.series.index(patch) + 1
1435 start = q.series.index(patch) + 1
1436 else:
1436 else:
1437 start = q.series_end()
1437 start = q.series_end()
1438 q.qseries(repo, start=start, summary=opts.get('summary'))
1438 q.qseries(repo, start=start, summary=opts.get('summary'))
1439
1439
1440 def qimport(ui, repo, *filename, **opts):
1440 def qimport(ui, repo, *filename, **opts):
1441 """import a patch
1441 """import a patch
1442
1442
1443 The patch will have the same name as its source file unless you
1443 The patch will have the same name as its source file unless you
1444 give it a new one with --name.
1444 give it a new one with --name.
1445
1445
1446 You can register an existing patch inside the patch directory
1446 You can register an existing patch inside the patch directory
1447 with the --existing flag.
1447 with the --existing flag.
1448
1448
1449 With --force, an existing patch of the same name will be overwritten.
1449 With --force, an existing patch of the same name will be overwritten.
1450
1450
1451 An existing changeset may be placed under mq control with --rev
1451 An existing changeset may be placed under mq control with --rev
1452 (e.g. qimport --rev tip -n patch will place tip under mq control).
1452 (e.g. qimport --rev tip -n patch will place tip under mq control).
1453 With --git, patches imported with --rev will use the git diff
1453 With --git, patches imported with --rev will use the git diff
1454 format.
1454 format.
1455 """
1455 """
1456 q = repo.mq
1456 q = repo.mq
1457 q.qimport(repo, filename, patchname=opts['name'],
1457 q.qimport(repo, filename, patchname=opts['name'],
1458 existing=opts['existing'], force=opts['force'], rev=opts['rev'],
1458 existing=opts['existing'], force=opts['force'], rev=opts['rev'],
1459 git=opts['git'])
1459 git=opts['git'])
1460 q.save_dirty()
1460 q.save_dirty()
1461 return 0
1461 return 0
1462
1462
1463 def init(ui, repo, **opts):
1463 def init(ui, repo, **opts):
1464 """init a new queue repository
1464 """init a new queue repository
1465
1465
1466 The queue repository is unversioned by default. If -c is
1466 The queue repository is unversioned by default. If -c is
1467 specified, qinit will create a separate nested repository
1467 specified, qinit will create a separate nested repository
1468 for patches. Use qcommit to commit changes to this queue
1468 for patches. Use qcommit to commit changes to this queue
1469 repository."""
1469 repository."""
1470 q = repo.mq
1470 q = repo.mq
1471 r = q.init(repo, create=opts['create_repo'])
1471 r = q.init(repo, create=opts['create_repo'])
1472 q.save_dirty()
1472 q.save_dirty()
1473 if r:
1473 if r:
1474 if not os.path.exists(r.wjoin('.hgignore')):
1474 if not os.path.exists(r.wjoin('.hgignore')):
1475 fp = r.wopener('.hgignore', 'w')
1475 fp = r.wopener('.hgignore', 'w')
1476 fp.write('syntax: glob\n')
1476 fp.write('syntax: glob\n')
1477 fp.write('status\n')
1477 fp.write('status\n')
1478 fp.write('guards\n')
1478 fp.write('guards\n')
1479 fp.close()
1479 fp.close()
1480 if not os.path.exists(r.wjoin('series')):
1480 if not os.path.exists(r.wjoin('series')):
1481 r.wopener('series', 'w').close()
1481 r.wopener('series', 'w').close()
1482 r.add(['.hgignore', 'series'])
1482 r.add(['.hgignore', 'series'])
1483 commands.add(ui, r)
1483 commands.add(ui, r)
1484 return 0
1484 return 0
1485
1485
1486 def clone(ui, source, dest=None, **opts):
1486 def clone(ui, source, dest=None, **opts):
1487 '''clone main and patch repository at same time
1487 '''clone main and patch repository at same time
1488
1488
1489 If source is local, destination will have no patches applied. If
1489 If source is local, destination will have no patches applied. If
1490 source is remote, this command can not check if patches are
1490 source is remote, this command can not check if patches are
1491 applied in source, so cannot guarantee that patches are not
1491 applied in source, so cannot guarantee that patches are not
1492 applied in destination. If you clone remote repository, be sure
1492 applied in destination. If you clone remote repository, be sure
1493 before that it has no patches applied.
1493 before that it has no patches applied.
1494
1494
1495 Source patch repository is looked for in <src>/.hg/patches by
1495 Source patch repository is looked for in <src>/.hg/patches by
1496 default. Use -p <url> to change.
1496 default. Use -p <url> to change.
1497 '''
1497 '''
1498 commands.setremoteconfig(ui, opts)
1498 commands.setremoteconfig(ui, opts)
1499 if dest is None:
1499 if dest is None:
1500 dest = hg.defaultdest(source)
1500 dest = hg.defaultdest(source)
1501 sr = hg.repository(ui, ui.expandpath(source))
1501 sr = hg.repository(ui, ui.expandpath(source))
1502 qbase, destrev = None, None
1502 qbase, destrev = None, None
1503 if sr.local():
1503 if sr.local():
1504 reposetup(ui, sr)
1505 if sr.mq.applied:
1504 if sr.mq.applied:
1506 qbase = revlog.bin(sr.mq.applied[0].rev)
1505 qbase = revlog.bin(sr.mq.applied[0].rev)
1507 if not hg.islocal(dest):
1506 if not hg.islocal(dest):
1508 destrev = sr.parents(qbase)[0]
1507 destrev = sr.parents(qbase)[0]
1509 ui.note(_('cloning main repo\n'))
1508 ui.note(_('cloning main repo\n'))
1510 sr, dr = hg.clone(ui, sr, dest,
1509 sr, dr = hg.clone(ui, sr, dest,
1511 pull=opts['pull'],
1510 pull=opts['pull'],
1512 rev=destrev,
1511 rev=destrev,
1513 update=False,
1512 update=False,
1514 stream=opts['uncompressed'])
1513 stream=opts['uncompressed'])
1515 ui.note(_('cloning patch repo\n'))
1514 ui.note(_('cloning patch repo\n'))
1516 spr, dpr = hg.clone(ui, opts['patches'] or (sr.url() + '/.hg/patches'),
1515 spr, dpr = hg.clone(ui, opts['patches'] or (sr.url() + '/.hg/patches'),
1517 dr.url() + '/.hg/patches',
1516 dr.url() + '/.hg/patches',
1518 pull=opts['pull'],
1517 pull=opts['pull'],
1519 update=not opts['noupdate'],
1518 update=not opts['noupdate'],
1520 stream=opts['uncompressed'])
1519 stream=opts['uncompressed'])
1521 if dr.local():
1520 if dr.local():
1522 if qbase:
1521 if qbase:
1523 ui.note(_('stripping applied patches from destination repo\n'))
1522 ui.note(_('stripping applied patches from destination repo\n'))
1524 reposetup(ui, dr)
1525 dr.mq.strip(dr, qbase, update=False, backup=None)
1523 dr.mq.strip(dr, qbase, update=False, backup=None)
1526 if not opts['noupdate']:
1524 if not opts['noupdate']:
1527 ui.note(_('updating destination repo\n'))
1525 ui.note(_('updating destination repo\n'))
1528 hg.update(dr, dr.changelog.tip())
1526 hg.update(dr, dr.changelog.tip())
1529
1527
1530 def commit(ui, repo, *pats, **opts):
1528 def commit(ui, repo, *pats, **opts):
1531 """commit changes in the queue repository"""
1529 """commit changes in the queue repository"""
1532 q = repo.mq
1530 q = repo.mq
1533 r = q.qrepo()
1531 r = q.qrepo()
1534 if not r: raise util.Abort('no queue repository')
1532 if not r: raise util.Abort('no queue repository')
1535 commands.commit(r.ui, r, *pats, **opts)
1533 commands.commit(r.ui, r, *pats, **opts)
1536
1534
1537 def series(ui, repo, **opts):
1535 def series(ui, repo, **opts):
1538 """print the entire series file"""
1536 """print the entire series file"""
1539 repo.mq.qseries(repo, missing=opts['missing'], summary=opts['summary'])
1537 repo.mq.qseries(repo, missing=opts['missing'], summary=opts['summary'])
1540 return 0
1538 return 0
1541
1539
1542 def top(ui, repo, **opts):
1540 def top(ui, repo, **opts):
1543 """print the name of the current patch"""
1541 """print the name of the current patch"""
1544 q = repo.mq
1542 q = repo.mq
1545 t = len(q.applied)
1543 t = len(q.applied)
1546 if t:
1544 if t:
1547 return q.qseries(repo, start=t-1, length=1, status='A',
1545 return q.qseries(repo, start=t-1, length=1, status='A',
1548 summary=opts.get('summary'))
1546 summary=opts.get('summary'))
1549 else:
1547 else:
1550 ui.write("No patches applied\n")
1548 ui.write("No patches applied\n")
1551 return 1
1549 return 1
1552
1550
1553 def next(ui, repo, **opts):
1551 def next(ui, repo, **opts):
1554 """print the name of the next patch"""
1552 """print the name of the next patch"""
1555 q = repo.mq
1553 q = repo.mq
1556 end = q.series_end()
1554 end = q.series_end()
1557 if end == len(q.series):
1555 if end == len(q.series):
1558 ui.write("All patches applied\n")
1556 ui.write("All patches applied\n")
1559 return 1
1557 return 1
1560 return q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
1558 return q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
1561
1559
1562 def prev(ui, repo, **opts):
1560 def prev(ui, repo, **opts):
1563 """print the name of the previous patch"""
1561 """print the name of the previous patch"""
1564 q = repo.mq
1562 q = repo.mq
1565 l = len(q.applied)
1563 l = len(q.applied)
1566 if l == 1:
1564 if l == 1:
1567 ui.write("Only one patch applied\n")
1565 ui.write("Only one patch applied\n")
1568 return 1
1566 return 1
1569 if not l:
1567 if not l:
1570 ui.write("No patches applied\n")
1568 ui.write("No patches applied\n")
1571 return 1
1569 return 1
1572 return q.qseries(repo, start=l-2, length=1, status='A',
1570 return q.qseries(repo, start=l-2, length=1, status='A',
1573 summary=opts.get('summary'))
1571 summary=opts.get('summary'))
1574
1572
1575 def new(ui, repo, patch, **opts):
1573 def new(ui, repo, patch, **opts):
1576 """create a new patch
1574 """create a new patch
1577
1575
1578 qnew creates a new patch on top of the currently-applied patch
1576 qnew creates a new patch on top of the currently-applied patch
1579 (if any). It will refuse to run if there are any outstanding
1577 (if any). It will refuse to run if there are any outstanding
1580 changes unless -f is specified, in which case the patch will
1578 changes unless -f is specified, in which case the patch will
1581 be initialised with them.
1579 be initialised with them.
1582
1580
1583 -e, -m or -l set the patch header as well as the commit message.
1581 -e, -m or -l set the patch header as well as the commit message.
1584 If none is specified, the patch header is empty and the
1582 If none is specified, the patch header is empty and the
1585 commit message is 'New patch: PATCH'"""
1583 commit message is 'New patch: PATCH'"""
1586 q = repo.mq
1584 q = repo.mq
1587 message = commands.logmessage(opts)
1585 message = commands.logmessage(opts)
1588 if opts['edit']:
1586 if opts['edit']:
1589 message = ui.edit(message, ui.username())
1587 message = ui.edit(message, ui.username())
1590 q.new(repo, patch, msg=message, force=opts['force'])
1588 q.new(repo, patch, msg=message, force=opts['force'])
1591 q.save_dirty()
1589 q.save_dirty()
1592 return 0
1590 return 0
1593
1591
1594 def refresh(ui, repo, *pats, **opts):
1592 def refresh(ui, repo, *pats, **opts):
1595 """update the current patch
1593 """update the current patch
1596
1594
1597 If any file patterns are provided, the refreshed patch will contain only
1595 If any file patterns are provided, the refreshed patch will contain only
1598 the modifications that match those patterns; the remaining modifications
1596 the modifications that match those patterns; the remaining modifications
1599 will remain in the working directory.
1597 will remain in the working directory.
1600
1598
1601 hg add/remove/copy/rename work as usual, though you might want to use
1599 hg add/remove/copy/rename work as usual, though you might want to use
1602 git-style patches (--git or [diff] git=1) to track copies and renames.
1600 git-style patches (--git or [diff] git=1) to track copies and renames.
1603 """
1601 """
1604 q = repo.mq
1602 q = repo.mq
1605 message = commands.logmessage(opts)
1603 message = commands.logmessage(opts)
1606 if opts['edit']:
1604 if opts['edit']:
1607 if message:
1605 if message:
1608 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1606 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1609 patch = q.applied[-1].name
1607 patch = q.applied[-1].name
1610 (message, comment, user, date, hasdiff) = q.readheaders(patch)
1608 (message, comment, user, date, hasdiff) = q.readheaders(patch)
1611 message = ui.edit('\n'.join(message), user or ui.username())
1609 message = ui.edit('\n'.join(message), user or ui.username())
1612 ret = q.refresh(repo, pats, msg=message, **opts)
1610 ret = q.refresh(repo, pats, msg=message, **opts)
1613 q.save_dirty()
1611 q.save_dirty()
1614 return ret
1612 return ret
1615
1613
1616 def diff(ui, repo, *pats, **opts):
1614 def diff(ui, repo, *pats, **opts):
1617 """diff of the current patch"""
1615 """diff of the current patch"""
1618 repo.mq.diff(repo, pats, opts)
1616 repo.mq.diff(repo, pats, opts)
1619 return 0
1617 return 0
1620
1618
1621 def fold(ui, repo, *files, **opts):
1619 def fold(ui, repo, *files, **opts):
1622 """fold the named patches into the current patch
1620 """fold the named patches into the current patch
1623
1621
1624 Patches must not yet be applied. Each patch will be successively
1622 Patches must not yet be applied. Each patch will be successively
1625 applied to the current patch in the order given. If all the
1623 applied to the current patch in the order given. If all the
1626 patches apply successfully, the current patch will be refreshed
1624 patches apply successfully, the current patch will be refreshed
1627 with the new cumulative patch, and the folded patches will
1625 with the new cumulative patch, and the folded patches will
1628 be deleted. With -k/--keep, the folded patch files will not
1626 be deleted. With -k/--keep, the folded patch files will not
1629 be removed afterwards.
1627 be removed afterwards.
1630
1628
1631 The header for each folded patch will be concatenated with
1629 The header for each folded patch will be concatenated with
1632 the current patch header, separated by a line of '* * *'."""
1630 the current patch header, separated by a line of '* * *'."""
1633
1631
1634 q = repo.mq
1632 q = repo.mq
1635
1633
1636 if not files:
1634 if not files:
1637 raise util.Abort(_('qfold requires at least one patch name'))
1635 raise util.Abort(_('qfold requires at least one patch name'))
1638 if not q.check_toppatch(repo):
1636 if not q.check_toppatch(repo):
1639 raise util.Abort(_('No patches applied'))
1637 raise util.Abort(_('No patches applied'))
1640
1638
1641 message = commands.logmessage(opts)
1639 message = commands.logmessage(opts)
1642 if opts['edit']:
1640 if opts['edit']:
1643 if message:
1641 if message:
1644 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1642 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1645
1643
1646 parent = q.lookup('qtip')
1644 parent = q.lookup('qtip')
1647 patches = []
1645 patches = []
1648 messages = []
1646 messages = []
1649 for f in files:
1647 for f in files:
1650 p = q.lookup(f)
1648 p = q.lookup(f)
1651 if p in patches or p == parent:
1649 if p in patches or p == parent:
1652 ui.warn(_('Skipping already folded patch %s') % p)
1650 ui.warn(_('Skipping already folded patch %s') % p)
1653 if q.isapplied(p):
1651 if q.isapplied(p):
1654 raise util.Abort(_('qfold cannot fold already applied patch %s') % p)
1652 raise util.Abort(_('qfold cannot fold already applied patch %s') % p)
1655 patches.append(p)
1653 patches.append(p)
1656
1654
1657 for p in patches:
1655 for p in patches:
1658 if not message:
1656 if not message:
1659 messages.append(q.readheaders(p)[0])
1657 messages.append(q.readheaders(p)[0])
1660 pf = q.join(p)
1658 pf = q.join(p)
1661 (patchsuccess, files, fuzz) = q.patch(repo, pf)
1659 (patchsuccess, files, fuzz) = q.patch(repo, pf)
1662 if not patchsuccess:
1660 if not patchsuccess:
1663 raise util.Abort(_('Error folding patch %s') % p)
1661 raise util.Abort(_('Error folding patch %s') % p)
1664 patch.updatedir(ui, repo, files)
1662 patch.updatedir(ui, repo, files)
1665
1663
1666 if not message:
1664 if not message:
1667 message, comments, user = q.readheaders(parent)[0:3]
1665 message, comments, user = q.readheaders(parent)[0:3]
1668 for msg in messages:
1666 for msg in messages:
1669 message.append('* * *')
1667 message.append('* * *')
1670 message.extend(msg)
1668 message.extend(msg)
1671 message = '\n'.join(message)
1669 message = '\n'.join(message)
1672
1670
1673 if opts['edit']:
1671 if opts['edit']:
1674 message = ui.edit(message, user or ui.username())
1672 message = ui.edit(message, user or ui.username())
1675
1673
1676 q.refresh(repo, msg=message)
1674 q.refresh(repo, msg=message)
1677 q.delete(repo, patches, opts)
1675 q.delete(repo, patches, opts)
1678 q.save_dirty()
1676 q.save_dirty()
1679
1677
1680 def guard(ui, repo, *args, **opts):
1678 def guard(ui, repo, *args, **opts):
1681 '''set or print guards for a patch
1679 '''set or print guards for a patch
1682
1680
1683 Guards control whether a patch can be pushed. A patch with no
1681 Guards control whether a patch can be pushed. A patch with no
1684 guards is always pushed. A patch with a positive guard ("+foo") is
1682 guards is always pushed. A patch with a positive guard ("+foo") is
1685 pushed only if the qselect command has activated it. A patch with
1683 pushed only if the qselect command has activated it. A patch with
1686 a negative guard ("-foo") is never pushed if the qselect command
1684 a negative guard ("-foo") is never pushed if the qselect command
1687 has activated it.
1685 has activated it.
1688
1686
1689 With no arguments, print the currently active guards.
1687 With no arguments, print the currently active guards.
1690 With arguments, set guards for the named patch.
1688 With arguments, set guards for the named patch.
1691
1689
1692 To set a negative guard "-foo" on topmost patch ("--" is needed so
1690 To set a negative guard "-foo" on topmost patch ("--" is needed so
1693 hg will not interpret "-foo" as an option):
1691 hg will not interpret "-foo" as an option):
1694 hg qguard -- -foo
1692 hg qguard -- -foo
1695
1693
1696 To set guards on another patch:
1694 To set guards on another patch:
1697 hg qguard other.patch +2.6.17 -stable
1695 hg qguard other.patch +2.6.17 -stable
1698 '''
1696 '''
1699 def status(idx):
1697 def status(idx):
1700 guards = q.series_guards[idx] or ['unguarded']
1698 guards = q.series_guards[idx] or ['unguarded']
1701 ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards)))
1699 ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards)))
1702 q = repo.mq
1700 q = repo.mq
1703 patch = None
1701 patch = None
1704 args = list(args)
1702 args = list(args)
1705 if opts['list']:
1703 if opts['list']:
1706 if args or opts['none']:
1704 if args or opts['none']:
1707 raise util.Abort(_('cannot mix -l/--list with options or arguments'))
1705 raise util.Abort(_('cannot mix -l/--list with options or arguments'))
1708 for i in xrange(len(q.series)):
1706 for i in xrange(len(q.series)):
1709 status(i)
1707 status(i)
1710 return
1708 return
1711 if not args or args[0][0:1] in '-+':
1709 if not args or args[0][0:1] in '-+':
1712 if not q.applied:
1710 if not q.applied:
1713 raise util.Abort(_('no patches applied'))
1711 raise util.Abort(_('no patches applied'))
1714 patch = q.applied[-1].name
1712 patch = q.applied[-1].name
1715 if patch is None and args[0][0:1] not in '-+':
1713 if patch is None and args[0][0:1] not in '-+':
1716 patch = args.pop(0)
1714 patch = args.pop(0)
1717 if patch is None:
1715 if patch is None:
1718 raise util.Abort(_('no patch to work with'))
1716 raise util.Abort(_('no patch to work with'))
1719 if args or opts['none']:
1717 if args or opts['none']:
1720 q.set_guards(q.find_series(patch), args)
1718 q.set_guards(q.find_series(patch), args)
1721 q.save_dirty()
1719 q.save_dirty()
1722 else:
1720 else:
1723 status(q.series.index(q.lookup(patch)))
1721 status(q.series.index(q.lookup(patch)))
1724
1722
1725 def header(ui, repo, patch=None):
1723 def header(ui, repo, patch=None):
1726 """Print the header of the topmost or specified patch"""
1724 """Print the header of the topmost or specified patch"""
1727 q = repo.mq
1725 q = repo.mq
1728
1726
1729 if patch:
1727 if patch:
1730 patch = q.lookup(patch)
1728 patch = q.lookup(patch)
1731 else:
1729 else:
1732 if not q.applied:
1730 if not q.applied:
1733 ui.write('No patches applied\n')
1731 ui.write('No patches applied\n')
1734 return 1
1732 return 1
1735 patch = q.lookup('qtip')
1733 patch = q.lookup('qtip')
1736 message = repo.mq.readheaders(patch)[0]
1734 message = repo.mq.readheaders(patch)[0]
1737
1735
1738 ui.write('\n'.join(message) + '\n')
1736 ui.write('\n'.join(message) + '\n')
1739
1737
1740 def lastsavename(path):
1738 def lastsavename(path):
1741 (directory, base) = os.path.split(path)
1739 (directory, base) = os.path.split(path)
1742 names = os.listdir(directory)
1740 names = os.listdir(directory)
1743 namere = re.compile("%s.([0-9]+)" % base)
1741 namere = re.compile("%s.([0-9]+)" % base)
1744 maxindex = None
1742 maxindex = None
1745 maxname = None
1743 maxname = None
1746 for f in names:
1744 for f in names:
1747 m = namere.match(f)
1745 m = namere.match(f)
1748 if m:
1746 if m:
1749 index = int(m.group(1))
1747 index = int(m.group(1))
1750 if maxindex == None or index > maxindex:
1748 if maxindex == None or index > maxindex:
1751 maxindex = index
1749 maxindex = index
1752 maxname = f
1750 maxname = f
1753 if maxname:
1751 if maxname:
1754 return (os.path.join(directory, maxname), maxindex)
1752 return (os.path.join(directory, maxname), maxindex)
1755 return (None, None)
1753 return (None, None)
1756
1754
1757 def savename(path):
1755 def savename(path):
1758 (last, index) = lastsavename(path)
1756 (last, index) = lastsavename(path)
1759 if last is None:
1757 if last is None:
1760 index = 0
1758 index = 0
1761 newpath = path + ".%d" % (index + 1)
1759 newpath = path + ".%d" % (index + 1)
1762 return newpath
1760 return newpath
1763
1761
1764 def push(ui, repo, patch=None, **opts):
1762 def push(ui, repo, patch=None, **opts):
1765 """push the next patch onto the stack"""
1763 """push the next patch onto the stack"""
1766 q = repo.mq
1764 q = repo.mq
1767 mergeq = None
1765 mergeq = None
1768
1766
1769 if opts['all']:
1767 if opts['all']:
1770 if not q.series:
1768 if not q.series:
1771 raise util.Abort(_('no patches in series'))
1769 raise util.Abort(_('no patches in series'))
1772 patch = q.series[-1]
1770 patch = q.series[-1]
1773 if opts['merge']:
1771 if opts['merge']:
1774 if opts['name']:
1772 if opts['name']:
1775 newpath = opts['name']
1773 newpath = opts['name']
1776 else:
1774 else:
1777 newpath, i = lastsavename(q.path)
1775 newpath, i = lastsavename(q.path)
1778 if not newpath:
1776 if not newpath:
1779 ui.warn("no saved queues found, please use -n\n")
1777 ui.warn("no saved queues found, please use -n\n")
1780 return 1
1778 return 1
1781 mergeq = queue(ui, repo.join(""), newpath)
1779 mergeq = queue(ui, repo.join(""), newpath)
1782 ui.warn("merging with queue at: %s\n" % mergeq.path)
1780 ui.warn("merging with queue at: %s\n" % mergeq.path)
1783 ret = q.push(repo, patch, force=opts['force'], list=opts['list'],
1781 ret = q.push(repo, patch, force=opts['force'], list=opts['list'],
1784 mergeq=mergeq)
1782 mergeq=mergeq)
1785 q.save_dirty()
1783 q.save_dirty()
1786 return ret
1784 return ret
1787
1785
1788 def pop(ui, repo, patch=None, **opts):
1786 def pop(ui, repo, patch=None, **opts):
1789 """pop the current patch off the stack"""
1787 """pop the current patch off the stack"""
1790 localupdate = True
1788 localupdate = True
1791 if opts['name']:
1789 if opts['name']:
1792 q = queue(ui, repo.join(""), repo.join(opts['name']))
1790 q = queue(ui, repo.join(""), repo.join(opts['name']))
1793 ui.warn('using patch queue: %s\n' % q.path)
1791 ui.warn('using patch queue: %s\n' % q.path)
1794 localupdate = False
1792 localupdate = False
1795 else:
1793 else:
1796 q = repo.mq
1794 q = repo.mq
1797 q.pop(repo, patch, force=opts['force'], update=localupdate, all=opts['all'])
1795 q.pop(repo, patch, force=opts['force'], update=localupdate, all=opts['all'])
1798 q.save_dirty()
1796 q.save_dirty()
1799 return 0
1797 return 0
1800
1798
1801 def rename(ui, repo, patch, name=None, **opts):
1799 def rename(ui, repo, patch, name=None, **opts):
1802 """rename a patch
1800 """rename a patch
1803
1801
1804 With one argument, renames the current patch to PATCH1.
1802 With one argument, renames the current patch to PATCH1.
1805 With two arguments, renames PATCH1 to PATCH2."""
1803 With two arguments, renames PATCH1 to PATCH2."""
1806
1804
1807 q = repo.mq
1805 q = repo.mq
1808
1806
1809 if not name:
1807 if not name:
1810 name = patch
1808 name = patch
1811 patch = None
1809 patch = None
1812
1810
1813 if patch:
1811 if patch:
1814 patch = q.lookup(patch)
1812 patch = q.lookup(patch)
1815 else:
1813 else:
1816 if not q.applied:
1814 if not q.applied:
1817 ui.write(_('No patches applied\n'))
1815 ui.write(_('No patches applied\n'))
1818 return
1816 return
1819 patch = q.lookup('qtip')
1817 patch = q.lookup('qtip')
1820 absdest = q.join(name)
1818 absdest = q.join(name)
1821 if os.path.isdir(absdest):
1819 if os.path.isdir(absdest):
1822 name = normname(os.path.join(name, os.path.basename(patch)))
1820 name = normname(os.path.join(name, os.path.basename(patch)))
1823 absdest = q.join(name)
1821 absdest = q.join(name)
1824 if os.path.exists(absdest):
1822 if os.path.exists(absdest):
1825 raise util.Abort(_('%s already exists') % absdest)
1823 raise util.Abort(_('%s already exists') % absdest)
1826
1824
1827 if name in q.series:
1825 if name in q.series:
1828 raise util.Abort(_('A patch named %s already exists in the series file') % name)
1826 raise util.Abort(_('A patch named %s already exists in the series file') % name)
1829
1827
1830 if ui.verbose:
1828 if ui.verbose:
1831 ui.write('Renaming %s to %s\n' % (patch, name))
1829 ui.write('Renaming %s to %s\n' % (patch, name))
1832 i = q.find_series(patch)
1830 i = q.find_series(patch)
1833 guards = q.guard_re.findall(q.full_series[i])
1831 guards = q.guard_re.findall(q.full_series[i])
1834 q.full_series[i] = name + ''.join([' #' + g for g in guards])
1832 q.full_series[i] = name + ''.join([' #' + g for g in guards])
1835 q.parse_series()
1833 q.parse_series()
1836 q.series_dirty = 1
1834 q.series_dirty = 1
1837
1835
1838 info = q.isapplied(patch)
1836 info = q.isapplied(patch)
1839 if info:
1837 if info:
1840 q.applied[info[0]] = statusentry(info[1], name)
1838 q.applied[info[0]] = statusentry(info[1], name)
1841 q.applied_dirty = 1
1839 q.applied_dirty = 1
1842
1840
1843 util.rename(q.join(patch), absdest)
1841 util.rename(q.join(patch), absdest)
1844 r = q.qrepo()
1842 r = q.qrepo()
1845 if r:
1843 if r:
1846 wlock = r.wlock()
1844 wlock = r.wlock()
1847 if r.dirstate.state(name) == 'r':
1845 if r.dirstate.state(name) == 'r':
1848 r.undelete([name], wlock)
1846 r.undelete([name], wlock)
1849 r.copy(patch, name, wlock)
1847 r.copy(patch, name, wlock)
1850 r.remove([patch], False, wlock)
1848 r.remove([patch], False, wlock)
1851
1849
1852 q.save_dirty()
1850 q.save_dirty()
1853
1851
1854 def restore(ui, repo, rev, **opts):
1852 def restore(ui, repo, rev, **opts):
1855 """restore the queue state saved by a rev"""
1853 """restore the queue state saved by a rev"""
1856 rev = repo.lookup(rev)
1854 rev = repo.lookup(rev)
1857 q = repo.mq
1855 q = repo.mq
1858 q.restore(repo, rev, delete=opts['delete'],
1856 q.restore(repo, rev, delete=opts['delete'],
1859 qupdate=opts['update'])
1857 qupdate=opts['update'])
1860 q.save_dirty()
1858 q.save_dirty()
1861 return 0
1859 return 0
1862
1860
1863 def save(ui, repo, **opts):
1861 def save(ui, repo, **opts):
1864 """save current queue state"""
1862 """save current queue state"""
1865 q = repo.mq
1863 q = repo.mq
1866 message = commands.logmessage(opts)
1864 message = commands.logmessage(opts)
1867 ret = q.save(repo, msg=message)
1865 ret = q.save(repo, msg=message)
1868 if ret:
1866 if ret:
1869 return ret
1867 return ret
1870 q.save_dirty()
1868 q.save_dirty()
1871 if opts['copy']:
1869 if opts['copy']:
1872 path = q.path
1870 path = q.path
1873 if opts['name']:
1871 if opts['name']:
1874 newpath = os.path.join(q.basepath, opts['name'])
1872 newpath = os.path.join(q.basepath, opts['name'])
1875 if os.path.exists(newpath):
1873 if os.path.exists(newpath):
1876 if not os.path.isdir(newpath):
1874 if not os.path.isdir(newpath):
1877 raise util.Abort(_('destination %s exists and is not '
1875 raise util.Abort(_('destination %s exists and is not '
1878 'a directory') % newpath)
1876 'a directory') % newpath)
1879 if not opts['force']:
1877 if not opts['force']:
1880 raise util.Abort(_('destination %s exists, '
1878 raise util.Abort(_('destination %s exists, '
1881 'use -f to force') % newpath)
1879 'use -f to force') % newpath)
1882 else:
1880 else:
1883 newpath = savename(path)
1881 newpath = savename(path)
1884 ui.warn("copy %s to %s\n" % (path, newpath))
1882 ui.warn("copy %s to %s\n" % (path, newpath))
1885 util.copyfiles(path, newpath)
1883 util.copyfiles(path, newpath)
1886 if opts['empty']:
1884 if opts['empty']:
1887 try:
1885 try:
1888 os.unlink(q.join(q.status_path))
1886 os.unlink(q.join(q.status_path))
1889 except:
1887 except:
1890 pass
1888 pass
1891 return 0
1889 return 0
1892
1890
1893 def strip(ui, repo, rev, **opts):
1891 def strip(ui, repo, rev, **opts):
1894 """strip a revision and all later revs on the same branch"""
1892 """strip a revision and all later revs on the same branch"""
1895 rev = repo.lookup(rev)
1893 rev = repo.lookup(rev)
1896 backup = 'all'
1894 backup = 'all'
1897 if opts['backup']:
1895 if opts['backup']:
1898 backup = 'strip'
1896 backup = 'strip'
1899 elif opts['nobackup']:
1897 elif opts['nobackup']:
1900 backup = 'none'
1898 backup = 'none'
1901 update = repo.dirstate.parents()[0] != revlog.nullid
1899 update = repo.dirstate.parents()[0] != revlog.nullid
1902 repo.mq.strip(repo, rev, backup=backup, update=update)
1900 repo.mq.strip(repo, rev, backup=backup, update=update)
1903 return 0
1901 return 0
1904
1902
1905 def select(ui, repo, *args, **opts):
1903 def select(ui, repo, *args, **opts):
1906 '''set or print guarded patches to push
1904 '''set or print guarded patches to push
1907
1905
1908 Use the qguard command to set or print guards on patch, then use
1906 Use the qguard command to set or print guards on patch, then use
1909 qselect to tell mq which guards to use. A patch will be pushed if it
1907 qselect to tell mq which guards to use. A patch will be pushed if it
1910 has no guards or any positive guards match the currently selected guard,
1908 has no guards or any positive guards match the currently selected guard,
1911 but will not be pushed if any negative guards match the current guard.
1909 but will not be pushed if any negative guards match the current guard.
1912 For example:
1910 For example:
1913
1911
1914 qguard foo.patch -stable (negative guard)
1912 qguard foo.patch -stable (negative guard)
1915 qguard bar.patch +stable (positive guard)
1913 qguard bar.patch +stable (positive guard)
1916 qselect stable
1914 qselect stable
1917
1915
1918 This activates the "stable" guard. mq will skip foo.patch (because
1916 This activates the "stable" guard. mq will skip foo.patch (because
1919 it has a negative match) but push bar.patch (because it
1917 it has a negative match) but push bar.patch (because it
1920 has a positive match).
1918 has a positive match).
1921
1919
1922 With no arguments, prints the currently active guards.
1920 With no arguments, prints the currently active guards.
1923 With one argument, sets the active guard.
1921 With one argument, sets the active guard.
1924
1922
1925 Use -n/--none to deactivate guards (no other arguments needed).
1923 Use -n/--none to deactivate guards (no other arguments needed).
1926 When no guards are active, patches with positive guards are skipped
1924 When no guards are active, patches with positive guards are skipped
1927 and patches with negative guards are pushed.
1925 and patches with negative guards are pushed.
1928
1926
1929 qselect can change the guards on applied patches. It does not pop
1927 qselect can change the guards on applied patches. It does not pop
1930 guarded patches by default. Use --pop to pop back to the last applied
1928 guarded patches by default. Use --pop to pop back to the last applied
1931 patch that is not guarded. Use --reapply (which implies --pop) to push
1929 patch that is not guarded. Use --reapply (which implies --pop) to push
1932 back to the current patch afterwards, but skip guarded patches.
1930 back to the current patch afterwards, but skip guarded patches.
1933
1931
1934 Use -s/--series to print a list of all guards in the series file (no
1932 Use -s/--series to print a list of all guards in the series file (no
1935 other arguments needed). Use -v for more information.'''
1933 other arguments needed). Use -v for more information.'''
1936
1934
1937 q = repo.mq
1935 q = repo.mq
1938 guards = q.active()
1936 guards = q.active()
1939 if args or opts['none']:
1937 if args or opts['none']:
1940 old_unapplied = q.unapplied(repo)
1938 old_unapplied = q.unapplied(repo)
1941 old_guarded = [i for i in xrange(len(q.applied)) if
1939 old_guarded = [i for i in xrange(len(q.applied)) if
1942 not q.pushable(i)[0]]
1940 not q.pushable(i)[0]]
1943 q.set_active(args)
1941 q.set_active(args)
1944 q.save_dirty()
1942 q.save_dirty()
1945 if not args:
1943 if not args:
1946 ui.status(_('guards deactivated\n'))
1944 ui.status(_('guards deactivated\n'))
1947 if not opts['pop'] and not opts['reapply']:
1945 if not opts['pop'] and not opts['reapply']:
1948 unapplied = q.unapplied(repo)
1946 unapplied = q.unapplied(repo)
1949 guarded = [i for i in xrange(len(q.applied))
1947 guarded = [i for i in xrange(len(q.applied))
1950 if not q.pushable(i)[0]]
1948 if not q.pushable(i)[0]]
1951 if len(unapplied) != len(old_unapplied):
1949 if len(unapplied) != len(old_unapplied):
1952 ui.status(_('number of unguarded, unapplied patches has '
1950 ui.status(_('number of unguarded, unapplied patches has '
1953 'changed from %d to %d\n') %
1951 'changed from %d to %d\n') %
1954 (len(old_unapplied), len(unapplied)))
1952 (len(old_unapplied), len(unapplied)))
1955 if len(guarded) != len(old_guarded):
1953 if len(guarded) != len(old_guarded):
1956 ui.status(_('number of guarded, applied patches has changed '
1954 ui.status(_('number of guarded, applied patches has changed '
1957 'from %d to %d\n') %
1955 'from %d to %d\n') %
1958 (len(old_guarded), len(guarded)))
1956 (len(old_guarded), len(guarded)))
1959 elif opts['series']:
1957 elif opts['series']:
1960 guards = {}
1958 guards = {}
1961 noguards = 0
1959 noguards = 0
1962 for gs in q.series_guards:
1960 for gs in q.series_guards:
1963 if not gs:
1961 if not gs:
1964 noguards += 1
1962 noguards += 1
1965 for g in gs:
1963 for g in gs:
1966 guards.setdefault(g, 0)
1964 guards.setdefault(g, 0)
1967 guards[g] += 1
1965 guards[g] += 1
1968 if ui.verbose:
1966 if ui.verbose:
1969 guards['NONE'] = noguards
1967 guards['NONE'] = noguards
1970 guards = guards.items()
1968 guards = guards.items()
1971 guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:]))
1969 guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:]))
1972 if guards:
1970 if guards:
1973 ui.note(_('guards in series file:\n'))
1971 ui.note(_('guards in series file:\n'))
1974 for guard, count in guards:
1972 for guard, count in guards:
1975 ui.note('%2d ' % count)
1973 ui.note('%2d ' % count)
1976 ui.write(guard, '\n')
1974 ui.write(guard, '\n')
1977 else:
1975 else:
1978 ui.note(_('no guards in series file\n'))
1976 ui.note(_('no guards in series file\n'))
1979 else:
1977 else:
1980 if guards:
1978 if guards:
1981 ui.note(_('active guards:\n'))
1979 ui.note(_('active guards:\n'))
1982 for g in guards:
1980 for g in guards:
1983 ui.write(g, '\n')
1981 ui.write(g, '\n')
1984 else:
1982 else:
1985 ui.write(_('no active guards\n'))
1983 ui.write(_('no active guards\n'))
1986 reapply = opts['reapply'] and q.applied and q.appliedname(-1)
1984 reapply = opts['reapply'] and q.applied and q.appliedname(-1)
1987 popped = False
1985 popped = False
1988 if opts['pop'] or opts['reapply']:
1986 if opts['pop'] or opts['reapply']:
1989 for i in xrange(len(q.applied)):
1987 for i in xrange(len(q.applied)):
1990 pushable, reason = q.pushable(i)
1988 pushable, reason = q.pushable(i)
1991 if not pushable:
1989 if not pushable:
1992 ui.status(_('popping guarded patches\n'))
1990 ui.status(_('popping guarded patches\n'))
1993 popped = True
1991 popped = True
1994 if i == 0:
1992 if i == 0:
1995 q.pop(repo, all=True)
1993 q.pop(repo, all=True)
1996 else:
1994 else:
1997 q.pop(repo, i-1)
1995 q.pop(repo, i-1)
1998 break
1996 break
1999 if popped:
1997 if popped:
2000 try:
1998 try:
2001 if reapply:
1999 if reapply:
2002 ui.status(_('reapplying unguarded patches\n'))
2000 ui.status(_('reapplying unguarded patches\n'))
2003 q.push(repo, reapply)
2001 q.push(repo, reapply)
2004 finally:
2002 finally:
2005 q.save_dirty()
2003 q.save_dirty()
2006
2004
2007 def reposetup(ui, repo):
2005 def reposetup(ui, repo):
2008 class mqrepo(repo.__class__):
2006 class mqrepo(repo.__class__):
2009 def abort_if_wdir_patched(self, errmsg, force=False):
2007 def abort_if_wdir_patched(self, errmsg, force=False):
2010 if self.mq.applied and not force:
2008 if self.mq.applied and not force:
2011 parent = revlog.hex(self.dirstate.parents()[0])
2009 parent = revlog.hex(self.dirstate.parents()[0])
2012 if parent in [s.rev for s in self.mq.applied]:
2010 if parent in [s.rev for s in self.mq.applied]:
2013 raise util.Abort(errmsg)
2011 raise util.Abort(errmsg)
2014
2012
2015 def commit(self, *args, **opts):
2013 def commit(self, *args, **opts):
2016 if len(args) >= 6:
2014 if len(args) >= 6:
2017 force = args[5]
2015 force = args[5]
2018 else:
2016 else:
2019 force = opts.get('force')
2017 force = opts.get('force')
2020 self.abort_if_wdir_patched(
2018 self.abort_if_wdir_patched(
2021 _('cannot commit over an applied mq patch'),
2019 _('cannot commit over an applied mq patch'),
2022 force)
2020 force)
2023
2021
2024 return super(mqrepo, self).commit(*args, **opts)
2022 return super(mqrepo, self).commit(*args, **opts)
2025
2023
2026 def push(self, remote, force=False, revs=None):
2024 def push(self, remote, force=False, revs=None):
2027 if self.mq.applied and not force and not revs:
2025 if self.mq.applied and not force and not revs:
2028 raise util.Abort(_('source has mq patches applied'))
2026 raise util.Abort(_('source has mq patches applied'))
2029 return super(mqrepo, self).push(remote, force, revs)
2027 return super(mqrepo, self).push(remote, force, revs)
2030
2028
2031 def tags(self):
2029 def tags(self):
2032 if self.tagscache:
2030 if self.tagscache:
2033 return self.tagscache
2031 return self.tagscache
2034
2032
2035 tagscache = super(mqrepo, self).tags()
2033 tagscache = super(mqrepo, self).tags()
2036
2034
2037 q = self.mq
2035 q = self.mq
2038 if not q.applied:
2036 if not q.applied:
2039 return tagscache
2037 return tagscache
2040
2038
2041 mqtags = [(patch.rev, patch.name) for patch in q.applied]
2039 mqtags = [(patch.rev, patch.name) for patch in q.applied]
2042 mqtags.append((mqtags[-1][0], 'qtip'))
2040 mqtags.append((mqtags[-1][0], 'qtip'))
2043 mqtags.append((mqtags[0][0], 'qbase'))
2041 mqtags.append((mqtags[0][0], 'qbase'))
2044 for patch in mqtags:
2042 for patch in mqtags:
2045 if patch[1] in tagscache:
2043 if patch[1] in tagscache:
2046 self.ui.warn('Tag %s overrides mq patch of the same name\n' % patch[1])
2044 self.ui.warn('Tag %s overrides mq patch of the same name\n' % patch[1])
2047 else:
2045 else:
2048 tagscache[patch[1]] = revlog.bin(patch[0])
2046 tagscache[patch[1]] = revlog.bin(patch[0])
2049
2047
2050 return tagscache
2048 return tagscache
2051
2049
2052 def _branchtags(self):
2050 def _branchtags(self):
2053 q = self.mq
2051 q = self.mq
2054 if not q.applied:
2052 if not q.applied:
2055 return super(mqrepo, self)._branchtags()
2053 return super(mqrepo, self)._branchtags()
2056
2054
2057 self.branchcache = {} # avoid recursion in changectx
2055 self.branchcache = {} # avoid recursion in changectx
2058 cl = self.changelog
2056 cl = self.changelog
2059 partial, last, lrev = self._readbranchcache()
2057 partial, last, lrev = self._readbranchcache()
2060
2058
2061 qbase = cl.rev(revlog.bin(q.applied[0].rev))
2059 qbase = cl.rev(revlog.bin(q.applied[0].rev))
2062 start = lrev + 1
2060 start = lrev + 1
2063 if start < qbase:
2061 if start < qbase:
2064 # update the cache (excluding the patches) and save it
2062 # update the cache (excluding the patches) and save it
2065 self._updatebranchcache(partial, lrev+1, qbase)
2063 self._updatebranchcache(partial, lrev+1, qbase)
2066 self._writebranchcache(partial, cl.node(qbase-1), qbase-1)
2064 self._writebranchcache(partial, cl.node(qbase-1), qbase-1)
2067 start = qbase
2065 start = qbase
2068 # if start = qbase, the cache is as updated as it should be.
2066 # if start = qbase, the cache is as updated as it should be.
2069 # if start > qbase, the cache includes (part of) the patches.
2067 # if start > qbase, the cache includes (part of) the patches.
2070 # we might as well use it, but we won't save it.
2068 # we might as well use it, but we won't save it.
2071
2069
2072 # update the cache up to the tip
2070 # update the cache up to the tip
2073 self._updatebranchcache(partial, start, cl.count())
2071 self._updatebranchcache(partial, start, cl.count())
2074
2072
2075 return partial
2073 return partial
2076
2074
2077 if repo.local():
2075 if repo.local():
2078 repo.__class__ = mqrepo
2076 repo.__class__ = mqrepo
2079 repo.mq = queue(ui, repo.join(""))
2077 repo.mq = queue(ui, repo.join(""))
2080
2078
2081 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
2079 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
2082
2080
2083 cmdtable = {
2081 cmdtable = {
2084 "qapplied": (applied, [] + seriesopts, 'hg qapplied [-s] [PATCH]'),
2082 "qapplied": (applied, [] + seriesopts, 'hg qapplied [-s] [PATCH]'),
2085 "qclone": (clone,
2083 "qclone": (clone,
2086 [('', 'pull', None, _('use pull protocol to copy metadata')),
2084 [('', 'pull', None, _('use pull protocol to copy metadata')),
2087 ('U', 'noupdate', None, _('do not update the new working directories')),
2085 ('U', 'noupdate', None, _('do not update the new working directories')),
2088 ('', 'uncompressed', None,
2086 ('', 'uncompressed', None,
2089 _('use uncompressed transfer (fast over LAN)')),
2087 _('use uncompressed transfer (fast over LAN)')),
2090 ('e', 'ssh', '', _('specify ssh command to use')),
2088 ('e', 'ssh', '', _('specify ssh command to use')),
2091 ('p', 'patches', '', _('location of source patch repo')),
2089 ('p', 'patches', '', _('location of source patch repo')),
2092 ('', 'remotecmd', '',
2090 ('', 'remotecmd', '',
2093 _('specify hg command to run on the remote side'))],
2091 _('specify hg command to run on the remote side'))],
2094 'hg qclone [OPTION]... SOURCE [DEST]'),
2092 'hg qclone [OPTION]... SOURCE [DEST]'),
2095 "qcommit|qci":
2093 "qcommit|qci":
2096 (commit,
2094 (commit,
2097 commands.table["^commit|ci"][1],
2095 commands.table["^commit|ci"][1],
2098 'hg qcommit [OPTION]... [FILE]...'),
2096 'hg qcommit [OPTION]... [FILE]...'),
2099 "^qdiff": (diff,
2097 "^qdiff": (diff,
2100 [('g', 'git', None, _('use git extended diff format')),
2098 [('g', 'git', None, _('use git extended diff format')),
2101 ('I', 'include', [], _('include names matching the given patterns')),
2099 ('I', 'include', [], _('include names matching the given patterns')),
2102 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2100 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2103 'hg qdiff [-I] [-X] [FILE]...'),
2101 'hg qdiff [-I] [-X] [FILE]...'),
2104 "qdelete|qremove|qrm":
2102 "qdelete|qremove|qrm":
2105 (delete,
2103 (delete,
2106 [('k', 'keep', None, _('keep patch file')),
2104 [('k', 'keep', None, _('keep patch file')),
2107 ('r', 'rev', [], _('stop managing a revision'))],
2105 ('r', 'rev', [], _('stop managing a revision'))],
2108 'hg qdelete [-k] [-r REV]... PATCH...'),
2106 'hg qdelete [-k] [-r REV]... PATCH...'),
2109 'qfold':
2107 'qfold':
2110 (fold,
2108 (fold,
2111 [('e', 'edit', None, _('edit patch header')),
2109 [('e', 'edit', None, _('edit patch header')),
2112 ('k', 'keep', None, _('keep folded patch files'))
2110 ('k', 'keep', None, _('keep folded patch files'))
2113 ] + commands.commitopts,
2111 ] + commands.commitopts,
2114 'hg qfold [-e] [-m <text>] [-l <file] PATCH...'),
2112 'hg qfold [-e] [-m <text>] [-l <file] PATCH...'),
2115 'qguard': (guard, [('l', 'list', None, _('list all patches and guards')),
2113 'qguard': (guard, [('l', 'list', None, _('list all patches and guards')),
2116 ('n', 'none', None, _('drop all guards'))],
2114 ('n', 'none', None, _('drop all guards'))],
2117 'hg qguard [PATCH] [+GUARD...] [-GUARD...]'),
2115 'hg qguard [PATCH] [+GUARD...] [-GUARD...]'),
2118 'qheader': (header, [],
2116 'qheader': (header, [],
2119 _('hg qheader [PATCH]')),
2117 _('hg qheader [PATCH]')),
2120 "^qimport":
2118 "^qimport":
2121 (qimport,
2119 (qimport,
2122 [('e', 'existing', None, 'import file in patch dir'),
2120 [('e', 'existing', None, 'import file in patch dir'),
2123 ('n', 'name', '', 'patch file name'),
2121 ('n', 'name', '', 'patch file name'),
2124 ('f', 'force', None, 'overwrite existing files'),
2122 ('f', 'force', None, 'overwrite existing files'),
2125 ('r', 'rev', [], 'place existing revisions under mq control'),
2123 ('r', 'rev', [], 'place existing revisions under mq control'),
2126 ('g', 'git', None, _('use git extended diff format'))],
2124 ('g', 'git', None, _('use git extended diff format'))],
2127 'hg qimport [-e] [-n NAME] [-f] [-g] [-r REV]... FILE...'),
2125 'hg qimport [-e] [-n NAME] [-f] [-g] [-r REV]... FILE...'),
2128 "^qinit":
2126 "^qinit":
2129 (init,
2127 (init,
2130 [('c', 'create-repo', None, 'create queue repository')],
2128 [('c', 'create-repo', None, 'create queue repository')],
2131 'hg qinit [-c]'),
2129 'hg qinit [-c]'),
2132 "qnew":
2130 "qnew":
2133 (new,
2131 (new,
2134 [('e', 'edit', None, _('edit commit message')),
2132 [('e', 'edit', None, _('edit commit message')),
2135 ('f', 'force', None, _('import uncommitted changes into patch'))
2133 ('f', 'force', None, _('import uncommitted changes into patch'))
2136 ] + commands.commitopts,
2134 ] + commands.commitopts,
2137 'hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH'),
2135 'hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH'),
2138 "qnext": (next, [] + seriesopts, 'hg qnext [-s]'),
2136 "qnext": (next, [] + seriesopts, 'hg qnext [-s]'),
2139 "qprev": (prev, [] + seriesopts, 'hg qprev [-s]'),
2137 "qprev": (prev, [] + seriesopts, 'hg qprev [-s]'),
2140 "^qpop":
2138 "^qpop":
2141 (pop,
2139 (pop,
2142 [('a', 'all', None, 'pop all patches'),
2140 [('a', 'all', None, 'pop all patches'),
2143 ('n', 'name', '', 'queue name to pop'),
2141 ('n', 'name', '', 'queue name to pop'),
2144 ('f', 'force', None, 'forget any local changes')],
2142 ('f', 'force', None, 'forget any local changes')],
2145 'hg qpop [-a] [-n NAME] [-f] [PATCH | INDEX]'),
2143 'hg qpop [-a] [-n NAME] [-f] [PATCH | INDEX]'),
2146 "^qpush":
2144 "^qpush":
2147 (push,
2145 (push,
2148 [('f', 'force', None, 'apply if the patch has rejects'),
2146 [('f', 'force', None, 'apply if the patch has rejects'),
2149 ('l', 'list', None, 'list patch name in commit text'),
2147 ('l', 'list', None, 'list patch name in commit text'),
2150 ('a', 'all', None, 'apply all patches'),
2148 ('a', 'all', None, 'apply all patches'),
2151 ('m', 'merge', None, 'merge from another queue'),
2149 ('m', 'merge', None, 'merge from another queue'),
2152 ('n', 'name', '', 'merge queue name')],
2150 ('n', 'name', '', 'merge queue name')],
2153 'hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]'),
2151 'hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]'),
2154 "^qrefresh":
2152 "^qrefresh":
2155 (refresh,
2153 (refresh,
2156 [('e', 'edit', None, _('edit commit message')),
2154 [('e', 'edit', None, _('edit commit message')),
2157 ('g', 'git', None, _('use git extended diff format')),
2155 ('g', 'git', None, _('use git extended diff format')),
2158 ('s', 'short', None, 'refresh only files already in the patch'),
2156 ('s', 'short', None, 'refresh only files already in the patch'),
2159 ('I', 'include', [], _('include names matching the given patterns')),
2157 ('I', 'include', [], _('include names matching the given patterns')),
2160 ('X', 'exclude', [], _('exclude names matching the given patterns'))
2158 ('X', 'exclude', [], _('exclude names matching the given patterns'))
2161 ] + commands.commitopts,
2159 ] + commands.commitopts,
2162 'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] FILES...'),
2160 'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] FILES...'),
2163 'qrename|qmv':
2161 'qrename|qmv':
2164 (rename, [], 'hg qrename PATCH1 [PATCH2]'),
2162 (rename, [], 'hg qrename PATCH1 [PATCH2]'),
2165 "qrestore":
2163 "qrestore":
2166 (restore,
2164 (restore,
2167 [('d', 'delete', None, 'delete save entry'),
2165 [('d', 'delete', None, 'delete save entry'),
2168 ('u', 'update', None, 'update queue working dir')],
2166 ('u', 'update', None, 'update queue working dir')],
2169 'hg qrestore [-d] [-u] REV'),
2167 'hg qrestore [-d] [-u] REV'),
2170 "qsave":
2168 "qsave":
2171 (save,
2169 (save,
2172 [('c', 'copy', None, 'copy patch directory'),
2170 [('c', 'copy', None, 'copy patch directory'),
2173 ('n', 'name', '', 'copy directory name'),
2171 ('n', 'name', '', 'copy directory name'),
2174 ('e', 'empty', None, 'clear queue status file'),
2172 ('e', 'empty', None, 'clear queue status file'),
2175 ('f', 'force', None, 'force copy')] + commands.commitopts,
2173 ('f', 'force', None, 'force copy')] + commands.commitopts,
2176 'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
2174 'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
2177 "qselect": (select,
2175 "qselect": (select,
2178 [('n', 'none', None, _('disable all guards')),
2176 [('n', 'none', None, _('disable all guards')),
2179 ('s', 'series', None, _('list all guards in series file')),
2177 ('s', 'series', None, _('list all guards in series file')),
2180 ('', 'pop', None,
2178 ('', 'pop', None,
2181 _('pop to before first guarded applied patch')),
2179 _('pop to before first guarded applied patch')),
2182 ('', 'reapply', None, _('pop, then reapply patches'))],
2180 ('', 'reapply', None, _('pop, then reapply patches'))],
2183 'hg qselect [OPTION...] [GUARD...]'),
2181 'hg qselect [OPTION...] [GUARD...]'),
2184 "qseries":
2182 "qseries":
2185 (series,
2183 (series,
2186 [('m', 'missing', None, 'print patches not in series')] + seriesopts,
2184 [('m', 'missing', None, 'print patches not in series')] + seriesopts,
2187 'hg qseries [-ms]'),
2185 'hg qseries [-ms]'),
2188 "^strip":
2186 "^strip":
2189 (strip,
2187 (strip,
2190 [('f', 'force', None, 'force multi-head removal'),
2188 [('f', 'force', None, 'force multi-head removal'),
2191 ('b', 'backup', None, 'bundle unrelated changesets'),
2189 ('b', 'backup', None, 'bundle unrelated changesets'),
2192 ('n', 'nobackup', None, 'no backups')],
2190 ('n', 'nobackup', None, 'no backups')],
2193 'hg strip [-f] [-b] [-n] REV'),
2191 'hg strip [-f] [-b] [-n] REV'),
2194 "qtop": (top, [] + seriesopts, 'hg qtop [-s]'),
2192 "qtop": (top, [] + seriesopts, 'hg qtop [-s]'),
2195 "qunapplied": (unapplied, [] + seriesopts, 'hg qunapplied [-s] [PATCH]'),
2193 "qunapplied": (unapplied, [] + seriesopts, 'hg qunapplied [-s] [PATCH]'),
2196 }
2194 }
@@ -1,281 +1,282 b''
1 # notify.py - email notifications for mercurial
1 # notify.py - email notifications for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 #
7 #
8 # hook extension to email notifications to people when changesets are
8 # hook extension to email notifications to people when changesets are
9 # committed to a repo they subscribe to.
9 # committed to a repo they subscribe to.
10 #
10 #
11 # default mode is to print messages to stdout, for testing and
11 # default mode is to print messages to stdout, for testing and
12 # configuring.
12 # configuring.
13 #
13 #
14 # to use, configure notify extension and enable in hgrc like this:
14 # to use, configure notify extension and enable in hgrc like this:
15 #
15 #
16 # [extensions]
16 # [extensions]
17 # hgext.notify =
17 # hgext.notify =
18 #
18 #
19 # [hooks]
19 # [hooks]
20 # # one email for each incoming changeset
20 # # one email for each incoming changeset
21 # incoming.notify = python:hgext.notify.hook
21 # incoming.notify = python:hgext.notify.hook
22 # # batch emails when many changesets incoming at one time
22 # # batch emails when many changesets incoming at one time
23 # changegroup.notify = python:hgext.notify.hook
23 # changegroup.notify = python:hgext.notify.hook
24 #
24 #
25 # [notify]
25 # [notify]
26 # # config items go in here
26 # # config items go in here
27 #
27 #
28 # config items:
28 # config items:
29 #
29 #
30 # REQUIRED:
30 # REQUIRED:
31 # config = /path/to/file # file containing subscriptions
31 # config = /path/to/file # file containing subscriptions
32 #
32 #
33 # OPTIONAL:
33 # OPTIONAL:
34 # test = True # print messages to stdout for testing
34 # test = True # print messages to stdout for testing
35 # strip = 3 # number of slashes to strip for url paths
35 # strip = 3 # number of slashes to strip for url paths
36 # domain = example.com # domain to use if committer missing domain
36 # domain = example.com # domain to use if committer missing domain
37 # style = ... # style file to use when formatting email
37 # style = ... # style file to use when formatting email
38 # template = ... # template to use when formatting email
38 # template = ... # template to use when formatting email
39 # incoming = ... # template to use when run as incoming hook
39 # incoming = ... # template to use when run as incoming hook
40 # changegroup = ... # template when run as changegroup hook
40 # changegroup = ... # template when run as changegroup hook
41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 # maxsubject = 67 # truncate subject line longer than this
42 # maxsubject = 67 # truncate subject line longer than this
43 # diffstat = True # add a diffstat before the diff content
43 # diffstat = True # add a diffstat before the diff content
44 # sources = serve # notify if source of incoming changes in this list
44 # sources = serve # notify if source of incoming changes in this list
45 # # (serve == ssh or http, push, pull, bundle)
45 # # (serve == ssh or http, push, pull, bundle)
46 # [email]
46 # [email]
47 # from = user@host.com # email address to send as if none given
47 # from = user@host.com # email address to send as if none given
48 # [web]
48 # [web]
49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
50 #
50 #
51 # notify config file has same format as regular hgrc. it has two
51 # notify config file has same format as regular hgrc. it has two
52 # sections so you can express subscriptions in whatever way is handier
52 # sections so you can express subscriptions in whatever way is handier
53 # for you.
53 # for you.
54 #
54 #
55 # [usersubs]
55 # [usersubs]
56 # # key is subscriber email, value is ","-separated list of glob patterns
56 # # key is subscriber email, value is ","-separated list of glob patterns
57 # user@host = pattern
57 # user@host = pattern
58 #
58 #
59 # [reposubs]
59 # [reposubs]
60 # # key is glob pattern, value is ","-separated list of subscriber emails
60 # # key is glob pattern, value is ","-separated list of subscriber emails
61 # pattern = user@host
61 # pattern = user@host
62 #
62 #
63 # glob patterns are matched against path to repo root.
63 # glob patterns are matched against path to repo root.
64 #
64 #
65 # if you like, you can put notify config file in repo that users can
65 # if you like, you can put notify config file in repo that users can
66 # push changes to, they can manage their own subscriptions.
66 # push changes to, they can manage their own subscriptions.
67
67
68 from mercurial.i18n import _
68 from mercurial.i18n import _
69 from mercurial.node import *
69 from mercurial.node import *
70 from mercurial import patch, cmdutil, templater, util, mail
70 from mercurial import patch, cmdutil, templater, util, mail
71 import email.Parser, fnmatch, socket, time
71 import email.Parser, fnmatch, socket, time
72
72
73 # template for single changeset can include email headers.
73 # template for single changeset can include email headers.
74 single_template = '''
74 single_template = '''
75 Subject: changeset in {webroot}: {desc|firstline|strip}
75 Subject: changeset in {webroot}: {desc|firstline|strip}
76 From: {author}
76 From: {author}
77
77
78 changeset {node|short} in {root}
78 changeset {node|short} in {root}
79 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
79 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
80 description:
80 description:
81 \t{desc|tabindent|strip}
81 \t{desc|tabindent|strip}
82 '''.lstrip()
82 '''.lstrip()
83
83
84 # template for multiple changesets should not contain email headers,
84 # template for multiple changesets should not contain email headers,
85 # because only first set of headers will be used and result will look
85 # because only first set of headers will be used and result will look
86 # strange.
86 # strange.
87 multiple_template = '''
87 multiple_template = '''
88 changeset {node|short} in {root}
88 changeset {node|short} in {root}
89 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
89 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
90 summary: {desc|firstline}
90 summary: {desc|firstline}
91 '''
91 '''
92
92
93 deftemplates = {
93 deftemplates = {
94 'changegroup': multiple_template,
94 'changegroup': multiple_template,
95 }
95 }
96
96
97 class notifier(object):
97 class notifier(object):
98 '''email notification class.'''
98 '''email notification class.'''
99
99
100 def __init__(self, ui, repo, hooktype):
100 def __init__(self, ui, repo, hooktype):
101 self.ui = ui
101 self.ui = ui
102 cfg = self.ui.config('notify', 'config')
102 cfg = self.ui.config('notify', 'config')
103 if cfg:
103 if cfg:
104 self.ui.readsections(cfg, 'usersubs', 'reposubs')
104 self.ui.readsections(cfg, 'usersubs', 'reposubs')
105 self.repo = repo
105 self.repo = repo
106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
107 self.root = self.strip(self.repo.root)
107 self.root = self.strip(self.repo.root)
108 self.domain = self.ui.config('notify', 'domain')
108 self.domain = self.ui.config('notify', 'domain')
109 self.subs = self.subscribers()
109 self.subs = self.subscribers()
110
110
111 mapfile = self.ui.config('notify', 'style')
111 mapfile = self.ui.config('notify', 'style')
112 template = (self.ui.config('notify', hooktype) or
112 template = (self.ui.config('notify', hooktype) or
113 self.ui.config('notify', 'template'))
113 self.ui.config('notify', 'template'))
114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
115 False, mapfile, False)
115 False, mapfile, False)
116 if not mapfile and not template:
116 if not mapfile and not template:
117 template = deftemplates.get(hooktype) or single_template
117 template = deftemplates.get(hooktype) or single_template
118 if template:
118 if template:
119 template = templater.parsestring(template, quoted=False)
119 template = templater.parsestring(template, quoted=False)
120 self.t.use_template(template)
120 self.t.use_template(template)
121
121
122 def strip(self, path):
122 def strip(self, path):
123 '''strip leading slashes from local path, turn into web-safe path.'''
123 '''strip leading slashes from local path, turn into web-safe path.'''
124
124
125 path = util.pconvert(path)
125 path = util.pconvert(path)
126 count = self.stripcount
126 count = self.stripcount
127 while count > 0:
127 while count > 0:
128 c = path.find('/')
128 c = path.find('/')
129 if c == -1:
129 if c == -1:
130 break
130 break
131 path = path[c+1:]
131 path = path[c+1:]
132 count -= 1
132 count -= 1
133 return path
133 return path
134
134
135 def fixmail(self, addr):
135 def fixmail(self, addr):
136 '''try to clean up email addresses.'''
136 '''try to clean up email addresses.'''
137
137
138 addr = templater.email(addr.strip())
138 addr = templater.email(addr.strip())
139 if self.domain:
139 a = addr.find('@localhost')
140 a = addr.find('@localhost')
140 if a != -1:
141 if a != -1:
141 addr = addr[:a]
142 addr = addr[:a]
142 if '@' not in addr:
143 if '@' not in addr:
143 return addr + '@' + self.domain
144 return addr + '@' + self.domain
144 return addr
145 return addr
145
146
146 def subscribers(self):
147 def subscribers(self):
147 '''return list of email addresses of subscribers to this repo.'''
148 '''return list of email addresses of subscribers to this repo.'''
148
149
149 subs = {}
150 subs = {}
150 for user, pats in self.ui.configitems('usersubs'):
151 for user, pats in self.ui.configitems('usersubs'):
151 for pat in pats.split(','):
152 for pat in pats.split(','):
152 if fnmatch.fnmatch(self.repo.root, pat.strip()):
153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
153 subs[self.fixmail(user)] = 1
154 subs[self.fixmail(user)] = 1
154 for pat, users in self.ui.configitems('reposubs'):
155 for pat, users in self.ui.configitems('reposubs'):
155 if fnmatch.fnmatch(self.repo.root, pat):
156 if fnmatch.fnmatch(self.repo.root, pat):
156 for user in users.split(','):
157 for user in users.split(','):
157 subs[self.fixmail(user)] = 1
158 subs[self.fixmail(user)] = 1
158 subs = subs.keys()
159 subs = subs.keys()
159 subs.sort()
160 subs.sort()
160 return subs
161 return subs
161
162
162 def url(self, path=None):
163 def url(self, path=None):
163 return self.ui.config('web', 'baseurl') + (path or self.root)
164 return self.ui.config('web', 'baseurl') + (path or self.root)
164
165
165 def node(self, node):
166 def node(self, node):
166 '''format one changeset.'''
167 '''format one changeset.'''
167
168
168 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
169 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
169 baseurl=self.ui.config('web', 'baseurl'),
170 baseurl=self.ui.config('web', 'baseurl'),
170 root=self.repo.root,
171 root=self.repo.root,
171 webroot=self.root)
172 webroot=self.root)
172
173
173 def skipsource(self, source):
174 def skipsource(self, source):
174 '''true if incoming changes from this source should be skipped.'''
175 '''true if incoming changes from this source should be skipped.'''
175 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 return source not in ok_sources
177 return source not in ok_sources
177
178
178 def send(self, node, count, data):
179 def send(self, node, count, data):
179 '''send message.'''
180 '''send message.'''
180
181
181 p = email.Parser.Parser()
182 p = email.Parser.Parser()
182 msg = p.parsestr(data)
183 msg = p.parsestr(data)
183
184
184 def fix_subject():
185 def fix_subject():
185 '''try to make subject line exist and be useful.'''
186 '''try to make subject line exist and be useful.'''
186
187
187 subject = msg['Subject']
188 subject = msg['Subject']
188 if not subject:
189 if not subject:
189 if count > 1:
190 if count > 1:
190 subject = _('%s: %d new changesets') % (self.root, count)
191 subject = _('%s: %d new changesets') % (self.root, count)
191 else:
192 else:
192 changes = self.repo.changelog.read(node)
193 changes = self.repo.changelog.read(node)
193 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
194 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
194 subject = '%s: %s' % (self.root, s)
195 subject = '%s: %s' % (self.root, s)
195 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
196 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
196 if maxsubject and len(subject) > maxsubject:
197 if maxsubject and len(subject) > maxsubject:
197 subject = subject[:maxsubject-3] + '...'
198 subject = subject[:maxsubject-3] + '...'
198 del msg['Subject']
199 del msg['Subject']
199 msg['Subject'] = subject
200 msg['Subject'] = subject
200
201
201 def fix_sender():
202 def fix_sender():
202 '''try to make message have proper sender.'''
203 '''try to make message have proper sender.'''
203
204
204 sender = msg['From']
205 sender = msg['From']
205 if not sender:
206 if not sender:
206 sender = self.ui.config('email', 'from') or self.ui.username()
207 sender = self.ui.config('email', 'from') or self.ui.username()
207 if '@' not in sender or '@localhost' in sender:
208 if '@' not in sender or '@localhost' in sender:
208 sender = self.fixmail(sender)
209 sender = self.fixmail(sender)
209 del msg['From']
210 del msg['From']
210 msg['From'] = sender
211 msg['From'] = sender
211
212
212 fix_subject()
213 fix_subject()
213 fix_sender()
214 fix_sender()
214
215
215 msg['X-Hg-Notification'] = 'changeset ' + short(node)
216 msg['X-Hg-Notification'] = 'changeset ' + short(node)
216 if not msg['Message-Id']:
217 if not msg['Message-Id']:
217 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
218 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
218 (short(node), int(time.time()),
219 (short(node), int(time.time()),
219 hash(self.repo.root), socket.getfqdn()))
220 hash(self.repo.root), socket.getfqdn()))
220 msg['To'] = ', '.join(self.subs)
221 msg['To'] = ', '.join(self.subs)
221
222
222 msgtext = msg.as_string(0)
223 msgtext = msg.as_string(0)
223 if self.ui.configbool('notify', 'test', True):
224 if self.ui.configbool('notify', 'test', True):
224 self.ui.write(msgtext)
225 self.ui.write(msgtext)
225 if not msgtext.endswith('\n'):
226 if not msgtext.endswith('\n'):
226 self.ui.write('\n')
227 self.ui.write('\n')
227 else:
228 else:
228 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
229 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
229 (len(self.subs), count))
230 (len(self.subs), count))
230 mail.sendmail(self.ui, templater.email(msg['From']),
231 mail.sendmail(self.ui, templater.email(msg['From']),
231 self.subs, msgtext)
232 self.subs, msgtext)
232
233
233 def diff(self, node, ref):
234 def diff(self, node, ref):
234 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
235 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
235 if maxdiff == 0:
236 if maxdiff == 0:
236 return
237 return
237 prev = self.repo.changelog.parents(node)[0]
238 prev = self.repo.changelog.parents(node)[0]
238 self.ui.pushbuffer()
239 self.ui.pushbuffer()
239 patch.diff(self.repo, prev, ref)
240 patch.diff(self.repo, prev, ref)
240 difflines = self.ui.popbuffer().splitlines(1)
241 difflines = self.ui.popbuffer().splitlines(1)
241 if self.ui.configbool('notify', 'diffstat', True):
242 if self.ui.configbool('notify', 'diffstat', True):
242 s = patch.diffstat(difflines)
243 s = patch.diffstat(difflines)
243 # s may be nil, don't include the header if it is
244 # s may be nil, don't include the header if it is
244 if s:
245 if s:
245 self.ui.write('\ndiffstat:\n\n%s' % s)
246 self.ui.write('\ndiffstat:\n\n%s' % s)
246 if maxdiff > 0 and len(difflines) > maxdiff:
247 if maxdiff > 0 and len(difflines) > maxdiff:
247 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
248 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
248 (len(difflines), maxdiff))
249 (len(difflines), maxdiff))
249 difflines = difflines[:maxdiff]
250 difflines = difflines[:maxdiff]
250 elif difflines:
251 elif difflines:
251 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
252 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
252 self.ui.write(*difflines)
253 self.ui.write(*difflines)
253
254
254 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
255 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
255 '''send email notifications to interested subscribers.
256 '''send email notifications to interested subscribers.
256
257
257 if used as changegroup hook, send one email for all changesets in
258 if used as changegroup hook, send one email for all changesets in
258 changegroup. else send one email per changeset.'''
259 changegroup. else send one email per changeset.'''
259 n = notifier(ui, repo, hooktype)
260 n = notifier(ui, repo, hooktype)
260 if not n.subs:
261 if not n.subs:
261 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
262 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
262 return
263 return
263 if n.skipsource(source):
264 if n.skipsource(source):
264 ui.debug(_('notify: changes have source "%s" - skipping\n') %
265 ui.debug(_('notify: changes have source "%s" - skipping\n') %
265 source)
266 source)
266 return
267 return
267 node = bin(node)
268 node = bin(node)
268 ui.pushbuffer()
269 ui.pushbuffer()
269 if hooktype == 'changegroup':
270 if hooktype == 'changegroup':
270 start = repo.changelog.rev(node)
271 start = repo.changelog.rev(node)
271 end = repo.changelog.count()
272 end = repo.changelog.count()
272 count = end - start
273 count = end - start
273 for rev in xrange(start, end):
274 for rev in xrange(start, end):
274 n.node(repo.changelog.node(rev))
275 n.node(repo.changelog.node(rev))
275 n.diff(node, repo.changelog.tip())
276 n.diff(node, repo.changelog.tip())
276 else:
277 else:
277 count = 1
278 count = 1
278 n.node(node)
279 n.node(node)
279 n.diff(node, node)
280 n.diff(node, node)
280 data = ui.popbuffer()
281 data = ui.popbuffer()
281 n.send(node, count, data)
282 n.send(node, count, data)
@@ -1,1152 +1,1156 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, mimetypes, re, zlib, mimetools, cStringIO, sys
9 import os, mimetypes, re, zlib, mimetools, cStringIO, sys
10 import tempfile, urllib, bz2
10 import tempfile, urllib, bz2
11 from mercurial.node import *
11 from mercurial.node import *
12 from mercurial.i18n import gettext as _
12 from mercurial.i18n import gettext as _
13 from mercurial import mdiff, ui, hg, util, archival, streamclone, patch
13 from mercurial import mdiff, ui, hg, util, archival, streamclone, patch
14 from mercurial import revlog, templater
14 from mercurial import revlog, templater
15 from common import get_mtime, staticfile, style_map
15 from common import get_mtime, staticfile, style_map
16
16
17 def _up(p):
17 def _up(p):
18 if p[0] != "/":
18 if p[0] != "/":
19 p = "/" + p
19 p = "/" + p
20 if p[-1] == "/":
20 if p[-1] == "/":
21 p = p[:-1]
21 p = p[:-1]
22 up = os.path.dirname(p)
22 up = os.path.dirname(p)
23 if up == "/":
23 if up == "/":
24 return "/"
24 return "/"
25 return up + "/"
25 return up + "/"
26
26
27 def revnavgen(pos, pagelen, limit, nodefunc):
27 def revnavgen(pos, pagelen, limit, nodefunc):
28 def seq(factor, limit=None):
28 def seq(factor, limit=None):
29 if limit:
29 if limit:
30 yield limit
30 yield limit
31 if limit >= 20 and limit <= 40:
31 if limit >= 20 and limit <= 40:
32 yield 50
32 yield 50
33 else:
33 else:
34 yield 1 * factor
34 yield 1 * factor
35 yield 3 * factor
35 yield 3 * factor
36 for f in seq(factor * 10):
36 for f in seq(factor * 10):
37 yield f
37 yield f
38
38
39 def nav(**map):
39 def nav(**map):
40 l = []
40 l = []
41 last = 0
41 last = 0
42 for f in seq(1, pagelen):
42 for f in seq(1, pagelen):
43 if f < pagelen or f <= last:
43 if f < pagelen or f <= last:
44 continue
44 continue
45 if f > limit:
45 if f > limit:
46 break
46 break
47 last = f
47 last = f
48 if pos + f < limit:
48 if pos + f < limit:
49 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
49 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
50 if pos - f >= 0:
50 if pos - f >= 0:
51 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
51 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
52
52
53 try:
53 try:
54 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
54 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
55
55
56 for label, node in l:
56 for label, node in l:
57 yield {"label": label, "node": node}
57 yield {"label": label, "node": node}
58
58
59 yield {"label": "tip", "node": "tip"}
59 yield {"label": "tip", "node": "tip"}
60 except hg.RepoError:
60 except hg.RepoError:
61 pass
61 pass
62
62
63 return nav
63 return nav
64
64
65 class hgweb(object):
65 class hgweb(object):
66 def __init__(self, repo, name=None):
66 def __init__(self, repo, name=None):
67 if type(repo) == type(""):
67 if type(repo) == type(""):
68 self.repo = hg.repository(ui.ui(report_untrusted=False), repo)
68 self.repo = hg.repository(ui.ui(report_untrusted=False), repo)
69 else:
69 else:
70 self.repo = repo
70 self.repo = repo
71
71
72 self.mtime = -1
72 self.mtime = -1
73 self.reponame = name
73 self.reponame = name
74 self.archives = 'zip', 'gz', 'bz2'
74 self.archives = 'zip', 'gz', 'bz2'
75 self.stripecount = 1
75 self.stripecount = 1
76 # a repo owner may set web.templates in .hg/hgrc to get any file
76 # a repo owner may set web.templates in .hg/hgrc to get any file
77 # readable by the user running the CGI script
77 # readable by the user running the CGI script
78 self.templatepath = self.config("web", "templates",
78 self.templatepath = self.config("web", "templates",
79 templater.templatepath(),
79 templater.templatepath(),
80 untrusted=False)
80 untrusted=False)
81
81
82 # The CGI scripts are often run by a user different from the repo owner.
82 # The CGI scripts are often run by a user different from the repo owner.
83 # Trust the settings from the .hg/hgrc files by default.
83 # Trust the settings from the .hg/hgrc files by default.
84 def config(self, section, name, default=None, untrusted=True):
84 def config(self, section, name, default=None, untrusted=True):
85 return self.repo.ui.config(section, name, default,
85 return self.repo.ui.config(section, name, default,
86 untrusted=untrusted)
86 untrusted=untrusted)
87
87
88 def configbool(self, section, name, default=False, untrusted=True):
88 def configbool(self, section, name, default=False, untrusted=True):
89 return self.repo.ui.configbool(section, name, default,
89 return self.repo.ui.configbool(section, name, default,
90 untrusted=untrusted)
90 untrusted=untrusted)
91
91
92 def configlist(self, section, name, default=None, untrusted=True):
92 def configlist(self, section, name, default=None, untrusted=True):
93 return self.repo.ui.configlist(section, name, default,
93 return self.repo.ui.configlist(section, name, default,
94 untrusted=untrusted)
94 untrusted=untrusted)
95
95
96 def refresh(self):
96 def refresh(self):
97 mtime = get_mtime(self.repo.root)
97 mtime = get_mtime(self.repo.root)
98 if mtime != self.mtime:
98 if mtime != self.mtime:
99 self.mtime = mtime
99 self.mtime = mtime
100 self.repo = hg.repository(self.repo.ui, self.repo.root)
100 self.repo = hg.repository(self.repo.ui, self.repo.root)
101 self.maxchanges = int(self.config("web", "maxchanges", 10))
101 self.maxchanges = int(self.config("web", "maxchanges", 10))
102 self.stripecount = int(self.config("web", "stripes", 1))
102 self.stripecount = int(self.config("web", "stripes", 1))
103 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
103 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
104 self.maxfiles = int(self.config("web", "maxfiles", 10))
104 self.maxfiles = int(self.config("web", "maxfiles", 10))
105 self.allowpull = self.configbool("web", "allowpull", True)
105 self.allowpull = self.configbool("web", "allowpull", True)
106
106
107 def archivelist(self, nodeid):
107 def archivelist(self, nodeid):
108 allowed = self.configlist("web", "allow_archive")
108 allowed = self.configlist("web", "allow_archive")
109 for i, spec in self.archive_specs.iteritems():
109 for i, spec in self.archive_specs.iteritems():
110 if i in allowed or self.configbool("web", "allow" + i):
110 if i in allowed or self.configbool("web", "allow" + i):
111 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
111 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
112
112
113 def listfilediffs(self, files, changeset):
113 def listfilediffs(self, files, changeset):
114 for f in files[:self.maxfiles]:
114 for f in files[:self.maxfiles]:
115 yield self.t("filedifflink", node=hex(changeset), file=f)
115 yield self.t("filedifflink", node=hex(changeset), file=f)
116 if len(files) > self.maxfiles:
116 if len(files) > self.maxfiles:
117 yield self.t("fileellipses")
117 yield self.t("fileellipses")
118
118
119 def siblings(self, siblings=[], hiderev=None, **args):
119 def siblings(self, siblings=[], hiderev=None, **args):
120 siblings = [s for s in siblings if s.node() != nullid]
120 siblings = [s for s in siblings if s.node() != nullid]
121 if len(siblings) == 1 and siblings[0].rev() == hiderev:
121 if len(siblings) == 1 and siblings[0].rev() == hiderev:
122 return
122 return
123 for s in siblings:
123 for s in siblings:
124 d = {'node': hex(s.node()), 'rev': s.rev()}
124 d = {'node': hex(s.node()), 'rev': s.rev()}
125 if hasattr(s, 'path'):
125 if hasattr(s, 'path'):
126 d['file'] = s.path()
126 d['file'] = s.path()
127 d.update(args)
127 d.update(args)
128 yield d
128 yield d
129
129
130 def renamelink(self, fl, node):
130 def renamelink(self, fl, node):
131 r = fl.renamed(node)
131 r = fl.renamed(node)
132 if r:
132 if r:
133 return [dict(file=r[0], node=hex(r[1]))]
133 return [dict(file=r[0], node=hex(r[1]))]
134 return []
134 return []
135
135
136 def showtag(self, t1, node=nullid, **args):
136 def showtag(self, t1, node=nullid, **args):
137 for t in self.repo.nodetags(node):
137 for t in self.repo.nodetags(node):
138 yield self.t(t1, tag=t, **args)
138 yield self.t(t1, tag=t, **args)
139
139
140 def diff(self, node1, node2, files):
140 def diff(self, node1, node2, files):
141 def filterfiles(filters, files):
141 def filterfiles(filters, files):
142 l = [x for x in files if x in filters]
142 l = [x for x in files if x in filters]
143
143
144 for t in filters:
144 for t in filters:
145 if t and t[-1] != os.sep:
145 if t and t[-1] != os.sep:
146 t += os.sep
146 t += os.sep
147 l += [x for x in files if x.startswith(t)]
147 l += [x for x in files if x.startswith(t)]
148 return l
148 return l
149
149
150 parity = [0]
150 parity = [0]
151 def diffblock(diff, f, fn):
151 def diffblock(diff, f, fn):
152 yield self.t("diffblock",
152 yield self.t("diffblock",
153 lines=prettyprintlines(diff),
153 lines=prettyprintlines(diff),
154 parity=parity[0],
154 parity=parity[0],
155 file=f,
155 file=f,
156 filenode=hex(fn or nullid))
156 filenode=hex(fn or nullid))
157 parity[0] = 1 - parity[0]
157 parity[0] = 1 - parity[0]
158
158
159 def prettyprintlines(diff):
159 def prettyprintlines(diff):
160 for l in diff.splitlines(1):
160 for l in diff.splitlines(1):
161 if l.startswith('+'):
161 if l.startswith('+'):
162 yield self.t("difflineplus", line=l)
162 yield self.t("difflineplus", line=l)
163 elif l.startswith('-'):
163 elif l.startswith('-'):
164 yield self.t("difflineminus", line=l)
164 yield self.t("difflineminus", line=l)
165 elif l.startswith('@'):
165 elif l.startswith('@'):
166 yield self.t("difflineat", line=l)
166 yield self.t("difflineat", line=l)
167 else:
167 else:
168 yield self.t("diffline", line=l)
168 yield self.t("diffline", line=l)
169
169
170 r = self.repo
170 r = self.repo
171 c1 = r.changectx(node1)
171 c1 = r.changectx(node1)
172 c2 = r.changectx(node2)
172 c2 = r.changectx(node2)
173 date1 = util.datestr(c1.date())
173 date1 = util.datestr(c1.date())
174 date2 = util.datestr(c2.date())
174 date2 = util.datestr(c2.date())
175
175
176 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
176 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
177 if files:
177 if files:
178 modified, added, removed = map(lambda x: filterfiles(files, x),
178 modified, added, removed = map(lambda x: filterfiles(files, x),
179 (modified, added, removed))
179 (modified, added, removed))
180
180
181 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
181 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
182 for f in modified:
182 for f in modified:
183 to = c1.filectx(f).data()
183 to = c1.filectx(f).data()
184 tn = c2.filectx(f).data()
184 tn = c2.filectx(f).data()
185 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
185 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
186 opts=diffopts), f, tn)
186 opts=diffopts), f, tn)
187 for f in added:
187 for f in added:
188 to = None
188 to = None
189 tn = c2.filectx(f).data()
189 tn = c2.filectx(f).data()
190 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
190 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
191 opts=diffopts), f, tn)
191 opts=diffopts), f, tn)
192 for f in removed:
192 for f in removed:
193 to = c1.filectx(f).data()
193 to = c1.filectx(f).data()
194 tn = None
194 tn = None
195 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
195 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
196 opts=diffopts), f, tn)
196 opts=diffopts), f, tn)
197
197
198 def changelog(self, ctx, shortlog=False):
198 def changelog(self, ctx, shortlog=False):
199 def changelist(**map):
199 def changelist(**map):
200 parity = (start - end) & 1
200 parity = (start - end) & 1
201 cl = self.repo.changelog
201 cl = self.repo.changelog
202 l = [] # build a list in forward order for efficiency
202 l = [] # build a list in forward order for efficiency
203 for i in xrange(start, end):
203 for i in xrange(start, end):
204 ctx = self.repo.changectx(i)
204 ctx = self.repo.changectx(i)
205 n = ctx.node()
205 n = ctx.node()
206
206
207 l.insert(0, {"parity": parity,
207 l.insert(0, {"parity": parity,
208 "author": ctx.user(),
208 "author": ctx.user(),
209 "parent": self.siblings(ctx.parents(), i - 1),
209 "parent": self.siblings(ctx.parents(), i - 1),
210 "child": self.siblings(ctx.children(), i + 1),
210 "child": self.siblings(ctx.children(), i + 1),
211 "changelogtag": self.showtag("changelogtag",n),
211 "changelogtag": self.showtag("changelogtag",n),
212 "desc": ctx.description(),
212 "desc": ctx.description(),
213 "date": ctx.date(),
213 "date": ctx.date(),
214 "files": self.listfilediffs(ctx.files(), n),
214 "files": self.listfilediffs(ctx.files(), n),
215 "rev": i,
215 "rev": i,
216 "node": hex(n)})
216 "node": hex(n)})
217 parity = 1 - parity
217 parity = 1 - parity
218
218
219 for e in l:
219 for e in l:
220 yield e
220 yield e
221
221
222 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
222 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
223 cl = self.repo.changelog
223 cl = self.repo.changelog
224 count = cl.count()
224 count = cl.count()
225 pos = ctx.rev()
225 pos = ctx.rev()
226 start = max(0, pos - maxchanges + 1)
226 start = max(0, pos - maxchanges + 1)
227 end = min(count, start + maxchanges)
227 end = min(count, start + maxchanges)
228 pos = end - 1
228 pos = end - 1
229
229
230 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
230 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
231
231
232 yield self.t(shortlog and 'shortlog' or 'changelog',
232 yield self.t(shortlog and 'shortlog' or 'changelog',
233 changenav=changenav,
233 changenav=changenav,
234 node=hex(cl.tip()),
234 node=hex(cl.tip()),
235 rev=pos, changesets=count, entries=changelist,
235 rev=pos, changesets=count, entries=changelist,
236 archives=self.archivelist("tip"))
236 archives=self.archivelist("tip"))
237
237
238 def search(self, query):
238 def search(self, query):
239
239
240 def changelist(**map):
240 def changelist(**map):
241 cl = self.repo.changelog
241 cl = self.repo.changelog
242 count = 0
242 count = 0
243 qw = query.lower().split()
243 qw = query.lower().split()
244
244
245 def revgen():
245 def revgen():
246 for i in xrange(cl.count() - 1, 0, -100):
246 for i in xrange(cl.count() - 1, 0, -100):
247 l = []
247 l = []
248 for j in xrange(max(0, i - 100), i):
248 for j in xrange(max(0, i - 100), i):
249 ctx = self.repo.changectx(j)
249 ctx = self.repo.changectx(j)
250 l.append(ctx)
250 l.append(ctx)
251 l.reverse()
251 l.reverse()
252 for e in l:
252 for e in l:
253 yield e
253 yield e
254
254
255 for ctx in revgen():
255 for ctx in revgen():
256 miss = 0
256 miss = 0
257 for q in qw:
257 for q in qw:
258 if not (q in ctx.user().lower() or
258 if not (q in ctx.user().lower() or
259 q in ctx.description().lower() or
259 q in ctx.description().lower() or
260 q in " ".join(ctx.files()[:20]).lower()):
260 q in " ".join(ctx.files()[:20]).lower()):
261 miss = 1
261 miss = 1
262 break
262 break
263 if miss:
263 if miss:
264 continue
264 continue
265
265
266 count += 1
266 count += 1
267 n = ctx.node()
267 n = ctx.node()
268
268
269 yield self.t('searchentry',
269 yield self.t('searchentry',
270 parity=self.stripes(count),
270 parity=self.stripes(count),
271 author=ctx.user(),
271 author=ctx.user(),
272 parent=self.siblings(ctx.parents()),
272 parent=self.siblings(ctx.parents()),
273 child=self.siblings(ctx.children()),
273 child=self.siblings(ctx.children()),
274 changelogtag=self.showtag("changelogtag",n),
274 changelogtag=self.showtag("changelogtag",n),
275 desc=ctx.description(),
275 desc=ctx.description(),
276 date=ctx.date(),
276 date=ctx.date(),
277 files=self.listfilediffs(ctx.files(), n),
277 files=self.listfilediffs(ctx.files(), n),
278 rev=ctx.rev(),
278 rev=ctx.rev(),
279 node=hex(n))
279 node=hex(n))
280
280
281 if count >= self.maxchanges:
281 if count >= self.maxchanges:
282 break
282 break
283
283
284 cl = self.repo.changelog
284 cl = self.repo.changelog
285
285
286 yield self.t('search',
286 yield self.t('search',
287 query=query,
287 query=query,
288 node=hex(cl.tip()),
288 node=hex(cl.tip()),
289 entries=changelist)
289 entries=changelist)
290
290
291 def changeset(self, ctx):
291 def changeset(self, ctx):
292 n = ctx.node()
292 n = ctx.node()
293 parents = ctx.parents()
293 parents = ctx.parents()
294 p1 = parents[0].node()
294 p1 = parents[0].node()
295
295
296 files = []
296 files = []
297 parity = 0
297 parity = 0
298 for f in ctx.files():
298 for f in ctx.files():
299 files.append(self.t("filenodelink",
299 files.append(self.t("filenodelink",
300 node=hex(n), file=f,
300 node=hex(n), file=f,
301 parity=parity))
301 parity=parity))
302 parity = 1 - parity
302 parity = 1 - parity
303
303
304 def diff(**map):
304 def diff(**map):
305 yield self.diff(p1, n, None)
305 yield self.diff(p1, n, None)
306
306
307 yield self.t('changeset',
307 yield self.t('changeset',
308 diff=diff,
308 diff=diff,
309 rev=ctx.rev(),
309 rev=ctx.rev(),
310 node=hex(n),
310 node=hex(n),
311 parent=self.siblings(parents),
311 parent=self.siblings(parents),
312 child=self.siblings(ctx.children()),
312 child=self.siblings(ctx.children()),
313 changesettag=self.showtag("changesettag",n),
313 changesettag=self.showtag("changesettag",n),
314 author=ctx.user(),
314 author=ctx.user(),
315 desc=ctx.description(),
315 desc=ctx.description(),
316 date=ctx.date(),
316 date=ctx.date(),
317 files=files,
317 files=files,
318 archives=self.archivelist(hex(n)))
318 archives=self.archivelist(hex(n)))
319
319
320 def filelog(self, fctx):
320 def filelog(self, fctx):
321 f = fctx.path()
321 f = fctx.path()
322 fl = fctx.filelog()
322 fl = fctx.filelog()
323 count = fl.count()
323 count = fl.count()
324 pagelen = self.maxshortchanges
324 pagelen = self.maxshortchanges
325 pos = fctx.filerev()
325 pos = fctx.filerev()
326 start = max(0, pos - pagelen + 1)
326 start = max(0, pos - pagelen + 1)
327 end = min(count, start + pagelen)
327 end = min(count, start + pagelen)
328 pos = end - 1
328 pos = end - 1
329
329
330 def entries(**map):
330 def entries(**map):
331 l = []
331 l = []
332 parity = (count - 1) & 1
332 parity = (count - 1) & 1
333
333
334 for i in xrange(start, end):
334 for i in xrange(start, end):
335 ctx = fctx.filectx(i)
335 ctx = fctx.filectx(i)
336 n = fl.node(i)
336 n = fl.node(i)
337
337
338 l.insert(0, {"parity": parity,
338 l.insert(0, {"parity": parity,
339 "filerev": i,
339 "filerev": i,
340 "file": f,
340 "file": f,
341 "node": hex(ctx.node()),
341 "node": hex(ctx.node()),
342 "author": ctx.user(),
342 "author": ctx.user(),
343 "date": ctx.date(),
343 "date": ctx.date(),
344 "rename": self.renamelink(fl, n),
344 "rename": self.renamelink(fl, n),
345 "parent": self.siblings(fctx.parents()),
345 "parent": self.siblings(fctx.parents()),
346 "child": self.siblings(fctx.children()),
346 "child": self.siblings(fctx.children()),
347 "desc": ctx.description()})
347 "desc": ctx.description()})
348 parity = 1 - parity
348 parity = 1 - parity
349
349
350 for e in l:
350 for e in l:
351 yield e
351 yield e
352
352
353 nodefunc = lambda x: fctx.filectx(fileid=x)
353 nodefunc = lambda x: fctx.filectx(fileid=x)
354 nav = revnavgen(pos, pagelen, count, nodefunc)
354 nav = revnavgen(pos, pagelen, count, nodefunc)
355 yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav,
355 yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav,
356 entries=entries)
356 entries=entries)
357
357
358 def filerevision(self, fctx):
358 def filerevision(self, fctx):
359 f = fctx.path()
359 f = fctx.path()
360 text = fctx.data()
360 text = fctx.data()
361 fl = fctx.filelog()
361 fl = fctx.filelog()
362 n = fctx.filenode()
362 n = fctx.filenode()
363
363
364 mt = mimetypes.guess_type(f)[0]
364 mt = mimetypes.guess_type(f)[0]
365 rawtext = text
365 rawtext = text
366 if util.binary(text):
366 if util.binary(text):
367 mt = mt or 'application/octet-stream'
367 mt = mt or 'application/octet-stream'
368 text = "(binary:%s)" % mt
368 text = "(binary:%s)" % mt
369 mt = mt or 'text/plain'
369 mt = mt or 'text/plain'
370
370
371 def lines():
371 def lines():
372 for l, t in enumerate(text.splitlines(1)):
372 for l, t in enumerate(text.splitlines(1)):
373 yield {"line": t,
373 yield {"line": t,
374 "linenumber": "% 6d" % (l + 1),
374 "linenumber": "% 6d" % (l + 1),
375 "parity": self.stripes(l)}
375 "parity": self.stripes(l)}
376
376
377 yield self.t("filerevision",
377 yield self.t("filerevision",
378 file=f,
378 file=f,
379 path=_up(f),
379 path=_up(f),
380 text=lines(),
380 text=lines(),
381 raw=rawtext,
381 raw=rawtext,
382 mimetype=mt,
382 mimetype=mt,
383 rev=fctx.rev(),
383 rev=fctx.rev(),
384 node=hex(fctx.node()),
384 node=hex(fctx.node()),
385 author=fctx.user(),
385 author=fctx.user(),
386 date=fctx.date(),
386 date=fctx.date(),
387 desc=fctx.description(),
387 desc=fctx.description(),
388 parent=self.siblings(fctx.parents()),
388 parent=self.siblings(fctx.parents()),
389 child=self.siblings(fctx.children()),
389 child=self.siblings(fctx.children()),
390 rename=self.renamelink(fl, n),
390 rename=self.renamelink(fl, n),
391 permissions=fctx.manifest().execf(f))
391 permissions=fctx.manifest().execf(f))
392
392
393 def fileannotate(self, fctx):
393 def fileannotate(self, fctx):
394 f = fctx.path()
394 f = fctx.path()
395 n = fctx.filenode()
395 n = fctx.filenode()
396 fl = fctx.filelog()
396 fl = fctx.filelog()
397
397
398 def annotate(**map):
398 def annotate(**map):
399 parity = 0
399 parity = 0
400 last = None
400 last = None
401 for f, l in fctx.annotate(follow=True):
401 for f, l in fctx.annotate(follow=True):
402 fnode = f.filenode()
402 fnode = f.filenode()
403 name = self.repo.ui.shortuser(f.user())
403 name = self.repo.ui.shortuser(f.user())
404
404
405 if last != fnode:
405 if last != fnode:
406 parity = 1 - parity
406 parity = 1 - parity
407 last = fnode
407 last = fnode
408
408
409 yield {"parity": parity,
409 yield {"parity": parity,
410 "node": hex(f.node()),
410 "node": hex(f.node()),
411 "rev": f.rev(),
411 "rev": f.rev(),
412 "author": name,
412 "author": name,
413 "file": f.path(),
413 "file": f.path(),
414 "line": l}
414 "line": l}
415
415
416 yield self.t("fileannotate",
416 yield self.t("fileannotate",
417 file=f,
417 file=f,
418 annotate=annotate,
418 annotate=annotate,
419 path=_up(f),
419 path=_up(f),
420 rev=fctx.rev(),
420 rev=fctx.rev(),
421 node=hex(fctx.node()),
421 node=hex(fctx.node()),
422 author=fctx.user(),
422 author=fctx.user(),
423 date=fctx.date(),
423 date=fctx.date(),
424 desc=fctx.description(),
424 desc=fctx.description(),
425 rename=self.renamelink(fl, n),
425 rename=self.renamelink(fl, n),
426 parent=self.siblings(fctx.parents()),
426 parent=self.siblings(fctx.parents()),
427 child=self.siblings(fctx.children()),
427 child=self.siblings(fctx.children()),
428 permissions=fctx.manifest().execf(f))
428 permissions=fctx.manifest().execf(f))
429
429
430 def manifest(self, ctx, path):
430 def manifest(self, ctx, path):
431 mf = ctx.manifest()
431 mf = ctx.manifest()
432 node = ctx.node()
432 node = ctx.node()
433
433
434 files = {}
434 files = {}
435
435
436 if path and path[-1] != "/":
436 if path and path[-1] != "/":
437 path += "/"
437 path += "/"
438 l = len(path)
438 l = len(path)
439 abspath = "/" + path
439 abspath = "/" + path
440
440
441 for f, n in mf.items():
441 for f, n in mf.items():
442 if f[:l] != path:
442 if f[:l] != path:
443 continue
443 continue
444 remain = f[l:]
444 remain = f[l:]
445 if "/" in remain:
445 if "/" in remain:
446 short = remain[:remain.index("/") + 1] # bleah
446 short = remain[:remain.index("/") + 1] # bleah
447 files[short] = (f, None)
447 files[short] = (f, None)
448 else:
448 else:
449 short = os.path.basename(remain)
449 short = os.path.basename(remain)
450 files[short] = (f, n)
450 files[short] = (f, n)
451
451
452 def filelist(**map):
452 def filelist(**map):
453 parity = 0
453 parity = 0
454 fl = files.keys()
454 fl = files.keys()
455 fl.sort()
455 fl.sort()
456 for f in fl:
456 for f in fl:
457 full, fnode = files[f]
457 full, fnode = files[f]
458 if not fnode:
458 if not fnode:
459 continue
459 continue
460
460
461 yield {"file": full,
461 yield {"file": full,
462 "parity": self.stripes(parity),
462 "parity": self.stripes(parity),
463 "basename": f,
463 "basename": f,
464 "size": ctx.filectx(full).size(),
464 "size": ctx.filectx(full).size(),
465 "permissions": mf.execf(full)}
465 "permissions": mf.execf(full)}
466 parity += 1
466 parity += 1
467
467
468 def dirlist(**map):
468 def dirlist(**map):
469 parity = 0
469 parity = 0
470 fl = files.keys()
470 fl = files.keys()
471 fl.sort()
471 fl.sort()
472 for f in fl:
472 for f in fl:
473 full, fnode = files[f]
473 full, fnode = files[f]
474 if fnode:
474 if fnode:
475 continue
475 continue
476
476
477 yield {"parity": self.stripes(parity),
477 yield {"parity": self.stripes(parity),
478 "path": os.path.join(abspath, f),
478 "path": os.path.join(abspath, f),
479 "basename": f[:-1]}
479 "basename": f[:-1]}
480 parity += 1
480 parity += 1
481
481
482 yield self.t("manifest",
482 yield self.t("manifest",
483 rev=ctx.rev(),
483 rev=ctx.rev(),
484 node=hex(node),
484 node=hex(node),
485 path=abspath,
485 path=abspath,
486 up=_up(abspath),
486 up=_up(abspath),
487 fentries=filelist,
487 fentries=filelist,
488 dentries=dirlist,
488 dentries=dirlist,
489 archives=self.archivelist(hex(node)))
489 archives=self.archivelist(hex(node)))
490
490
491 def tags(self):
491 def tags(self):
492 i = self.repo.tagslist()
492 i = self.repo.tagslist()
493 i.reverse()
493 i.reverse()
494
494
495 def entries(notip=False, **map):
495 def entries(notip=False, **map):
496 parity = 0
496 parity = 0
497 for k, n in i:
497 for k, n in i:
498 if notip and k == "tip":
498 if notip and k == "tip":
499 continue
499 continue
500 yield {"parity": self.stripes(parity),
500 yield {"parity": self.stripes(parity),
501 "tag": k,
501 "tag": k,
502 "date": self.repo.changectx(n).date(),
502 "date": self.repo.changectx(n).date(),
503 "node": hex(n)}
503 "node": hex(n)}
504 parity += 1
504 parity += 1
505
505
506 yield self.t("tags",
506 yield self.t("tags",
507 node=hex(self.repo.changelog.tip()),
507 node=hex(self.repo.changelog.tip()),
508 entries=lambda **x: entries(False, **x),
508 entries=lambda **x: entries(False, **x),
509 entriesnotip=lambda **x: entries(True, **x))
509 entriesnotip=lambda **x: entries(True, **x))
510
510
511 def summary(self):
511 def summary(self):
512 i = self.repo.tagslist()
512 i = self.repo.tagslist()
513 i.reverse()
513 i.reverse()
514
514
515 def tagentries(**map):
515 def tagentries(**map):
516 parity = 0
516 parity = 0
517 count = 0
517 count = 0
518 for k, n in i:
518 for k, n in i:
519 if k == "tip": # skip tip
519 if k == "tip": # skip tip
520 continue;
520 continue;
521
521
522 count += 1
522 count += 1
523 if count > 10: # limit to 10 tags
523 if count > 10: # limit to 10 tags
524 break;
524 break;
525
525
526 yield self.t("tagentry",
526 yield self.t("tagentry",
527 parity=self.stripes(parity),
527 parity=self.stripes(parity),
528 tag=k,
528 tag=k,
529 node=hex(n),
529 node=hex(n),
530 date=self.repo.changectx(n).date())
530 date=self.repo.changectx(n).date())
531 parity += 1
531 parity += 1
532
532
533 def heads(**map):
533 def heads(**map):
534 parity = 0
534 parity = 0
535 count = 0
535 count = 0
536
536
537 for node in self.repo.heads():
537 for node in self.repo.heads():
538 count += 1
538 count += 1
539 if count > 10:
539 if count > 10:
540 break;
540 break;
541
541
542 ctx = self.repo.changectx(node)
542 ctx = self.repo.changectx(node)
543
543
544 yield {'parity': self.stripes(parity),
544 yield {'parity': self.stripes(parity),
545 'branch': ctx.branch(),
545 'branch': ctx.branch(),
546 'node': hex(node),
546 'node': hex(node),
547 'date': ctx.date()}
547 'date': ctx.date()}
548 parity += 1
548 parity += 1
549
549
550 def changelist(**map):
550 def changelist(**map):
551 parity = 0
551 parity = 0
552 l = [] # build a list in forward order for efficiency
552 l = [] # build a list in forward order for efficiency
553 for i in xrange(start, end):
553 for i in xrange(start, end):
554 ctx = self.repo.changectx(i)
554 ctx = self.repo.changectx(i)
555 hn = hex(ctx.node())
555 hn = hex(ctx.node())
556
556
557 l.insert(0, self.t(
557 l.insert(0, self.t(
558 'shortlogentry',
558 'shortlogentry',
559 parity=parity,
559 parity=parity,
560 author=ctx.user(),
560 author=ctx.user(),
561 desc=ctx.description(),
561 desc=ctx.description(),
562 date=ctx.date(),
562 date=ctx.date(),
563 rev=i,
563 rev=i,
564 node=hn))
564 node=hn))
565 parity = 1 - parity
565 parity = 1 - parity
566
566
567 yield l
567 yield l
568
568
569 cl = self.repo.changelog
569 cl = self.repo.changelog
570 count = cl.count()
570 count = cl.count()
571 start = max(0, count - self.maxchanges)
571 start = max(0, count - self.maxchanges)
572 end = min(count, start + self.maxchanges)
572 end = min(count, start + self.maxchanges)
573
573
574 yield self.t("summary",
574 yield self.t("summary",
575 desc=self.config("web", "description", "unknown"),
575 desc=self.config("web", "description", "unknown"),
576 owner=(self.config("ui", "username") or # preferred
576 owner=(self.config("ui", "username") or # preferred
577 self.config("web", "contact") or # deprecated
577 self.config("web", "contact") or # deprecated
578 self.config("web", "author", "unknown")), # also
578 self.config("web", "author", "unknown")), # also
579 lastchange=cl.read(cl.tip())[2],
579 lastchange=cl.read(cl.tip())[2],
580 tags=tagentries,
580 tags=tagentries,
581 heads=heads,
581 heads=heads,
582 shortlog=changelist,
582 shortlog=changelist,
583 node=hex(cl.tip()),
583 node=hex(cl.tip()),
584 archives=self.archivelist("tip"))
584 archives=self.archivelist("tip"))
585
585
586 def filediff(self, fctx):
586 def filediff(self, fctx):
587 n = fctx.node()
587 n = fctx.node()
588 path = fctx.path()
588 path = fctx.path()
589 parents = fctx.parents()
589 parents = fctx.parents()
590 p1 = parents and parents[0].node() or nullid
590 p1 = parents and parents[0].node() or nullid
591
591
592 def diff(**map):
592 def diff(**map):
593 yield self.diff(p1, n, [path])
593 yield self.diff(p1, n, [path])
594
594
595 yield self.t("filediff",
595 yield self.t("filediff",
596 file=path,
596 file=path,
597 node=hex(n),
597 node=hex(n),
598 rev=fctx.rev(),
598 rev=fctx.rev(),
599 parent=self.siblings(parents),
599 parent=self.siblings(parents),
600 child=self.siblings(fctx.children()),
600 child=self.siblings(fctx.children()),
601 diff=diff)
601 diff=diff)
602
602
603 archive_specs = {
603 archive_specs = {
604 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
604 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
605 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
605 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
606 'zip': ('application/zip', 'zip', '.zip', None),
606 'zip': ('application/zip', 'zip', '.zip', None),
607 }
607 }
608
608
609 def archive(self, req, cnode, type_):
609 def archive(self, req, cnode, type_):
610 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
610 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
611 name = "%s-%s" % (reponame, short(cnode))
611 name = "%s-%s" % (reponame, short(cnode))
612 mimetype, artype, extension, encoding = self.archive_specs[type_]
612 mimetype, artype, extension, encoding = self.archive_specs[type_]
613 headers = [('Content-type', mimetype),
613 headers = [('Content-type', mimetype),
614 ('Content-disposition', 'attachment; filename=%s%s' %
614 ('Content-disposition', 'attachment; filename=%s%s' %
615 (name, extension))]
615 (name, extension))]
616 if encoding:
616 if encoding:
617 headers.append(('Content-encoding', encoding))
617 headers.append(('Content-encoding', encoding))
618 req.header(headers)
618 req.header(headers)
619 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
619 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
620
620
621 # add tags to things
621 # add tags to things
622 # tags -> list of changesets corresponding to tags
622 # tags -> list of changesets corresponding to tags
623 # find tag, changeset, file
623 # find tag, changeset, file
624
624
625 def cleanpath(self, path):
625 def cleanpath(self, path):
626 path = path.lstrip('/')
626 path = path.lstrip('/')
627 return util.canonpath(self.repo.root, '', path)
627 return util.canonpath(self.repo.root, '', path)
628
628
629 def run(self):
629 def run(self):
630 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
630 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
631 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
631 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
632 import mercurial.hgweb.wsgicgi as wsgicgi
632 import mercurial.hgweb.wsgicgi as wsgicgi
633 from request import wsgiapplication
633 from request import wsgiapplication
634 def make_web_app():
634 def make_web_app():
635 return self
635 return self
636 wsgicgi.launch(wsgiapplication(make_web_app))
636 wsgicgi.launch(wsgiapplication(make_web_app))
637
637
638 def run_wsgi(self, req):
638 def run_wsgi(self, req):
639 def header(**map):
639 def header(**map):
640 header_file = cStringIO.StringIO(
640 header_file = cStringIO.StringIO(
641 ''.join(self.t("header", encoding=util._encoding, **map)))
641 ''.join(self.t("header", encoding=util._encoding, **map)))
642 msg = mimetools.Message(header_file, 0)
642 msg = mimetools.Message(header_file, 0)
643 req.header(msg.items())
643 req.header(msg.items())
644 yield header_file.read()
644 yield header_file.read()
645
645
646 def rawfileheader(**map):
646 def rawfileheader(**map):
647 req.header([('Content-type', map['mimetype']),
647 req.header([('Content-type', map['mimetype']),
648 ('Content-disposition', 'filename=%s' % map['file']),
648 ('Content-disposition', 'filename=%s' % map['file']),
649 ('Content-length', str(len(map['raw'])))])
649 ('Content-length', str(len(map['raw'])))])
650 yield ''
650 yield ''
651
651
652 def footer(**map):
652 def footer(**map):
653 yield self.t("footer", **map)
653 yield self.t("footer", **map)
654
654
655 def motd(**map):
655 def motd(**map):
656 yield self.config("web", "motd", "")
656 yield self.config("web", "motd", "")
657
657
658 def expand_form(form):
658 def expand_form(form):
659 shortcuts = {
659 shortcuts = {
660 'cl': [('cmd', ['changelog']), ('rev', None)],
660 'cl': [('cmd', ['changelog']), ('rev', None)],
661 'sl': [('cmd', ['shortlog']), ('rev', None)],
661 'sl': [('cmd', ['shortlog']), ('rev', None)],
662 'cs': [('cmd', ['changeset']), ('node', None)],
662 'cs': [('cmd', ['changeset']), ('node', None)],
663 'f': [('cmd', ['file']), ('filenode', None)],
663 'f': [('cmd', ['file']), ('filenode', None)],
664 'fl': [('cmd', ['filelog']), ('filenode', None)],
664 'fl': [('cmd', ['filelog']), ('filenode', None)],
665 'fd': [('cmd', ['filediff']), ('node', None)],
665 'fd': [('cmd', ['filediff']), ('node', None)],
666 'fa': [('cmd', ['annotate']), ('filenode', None)],
666 'fa': [('cmd', ['annotate']), ('filenode', None)],
667 'mf': [('cmd', ['manifest']), ('manifest', None)],
667 'mf': [('cmd', ['manifest']), ('manifest', None)],
668 'ca': [('cmd', ['archive']), ('node', None)],
668 'ca': [('cmd', ['archive']), ('node', None)],
669 'tags': [('cmd', ['tags'])],
669 'tags': [('cmd', ['tags'])],
670 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
670 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
671 'static': [('cmd', ['static']), ('file', None)]
671 'static': [('cmd', ['static']), ('file', None)]
672 }
672 }
673
673
674 for k in shortcuts.iterkeys():
674 for k in shortcuts.iterkeys():
675 if form.has_key(k):
675 if form.has_key(k):
676 for name, value in shortcuts[k]:
676 for name, value in shortcuts[k]:
677 if value is None:
677 if value is None:
678 value = form[k]
678 value = form[k]
679 form[name] = value
679 form[name] = value
680 del form[k]
680 del form[k]
681
681
682 def rewrite_request(req):
682 def rewrite_request(req):
683 '''translate new web interface to traditional format'''
683 '''translate new web interface to traditional format'''
684
684
685 def spliturl(req):
685 def spliturl(req):
686 def firstitem(query):
686 def firstitem(query):
687 return query.split('&', 1)[0].split(';', 1)[0]
687 return query.split('&', 1)[0].split(';', 1)[0]
688
688
689 def normurl(url):
689 def normurl(url):
690 inner = '/'.join([x for x in url.split('/') if x])
690 inner = '/'.join([x for x in url.split('/') if x])
691 tl = len(url) > 1 and url.endswith('/') and '/' or ''
691 tl = len(url) > 1 and url.endswith('/') and '/' or ''
692
692
693 return '%s%s%s' % (url.startswith('/') and '/' or '',
693 return '%s%s%s' % (url.startswith('/') and '/' or '',
694 inner, tl)
694 inner, tl)
695
695
696 root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0]))
696 root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0]))
697 pi = normurl(req.env.get('PATH_INFO', ''))
697 pi = normurl(req.env.get('PATH_INFO', ''))
698 if pi:
698 if pi:
699 # strip leading /
699 # strip leading /
700 pi = pi[1:]
700 pi = pi[1:]
701 if pi:
701 if pi:
702 root = root[:-len(pi)]
702 root = root[:-len(pi)]
703 if req.env.has_key('REPO_NAME'):
703 if req.env.has_key('REPO_NAME'):
704 rn = req.env['REPO_NAME'] + '/'
704 rn = req.env['REPO_NAME'] + '/'
705 root += rn
705 root += rn
706 query = pi[len(rn):]
706 query = pi[len(rn):]
707 else:
707 else:
708 query = pi
708 query = pi
709 else:
709 else:
710 root += '?'
710 root += '?'
711 query = firstitem(req.env['QUERY_STRING'])
711 query = firstitem(req.env['QUERY_STRING'])
712
712
713 return (root, query)
713 return (root, query)
714
714
715 req.url, query = spliturl(req)
715 req.url, query = spliturl(req)
716
716
717 if req.form.has_key('cmd'):
717 if req.form.has_key('cmd'):
718 # old style
718 # old style
719 return
719 return
720
720
721 args = query.split('/', 2)
721 args = query.split('/', 2)
722 if not args or not args[0]:
722 if not args or not args[0]:
723 return
723 return
724
724
725 cmd = args.pop(0)
725 cmd = args.pop(0)
726 style = cmd.rfind('-')
726 style = cmd.rfind('-')
727 if style != -1:
727 if style != -1:
728 req.form['style'] = [cmd[:style]]
728 req.form['style'] = [cmd[:style]]
729 cmd = cmd[style+1:]
729 cmd = cmd[style+1:]
730 # avoid accepting e.g. style parameter as command
730 # avoid accepting e.g. style parameter as command
731 if hasattr(self, 'do_' + cmd):
731 if hasattr(self, 'do_' + cmd):
732 req.form['cmd'] = [cmd]
732 req.form['cmd'] = [cmd]
733
733
734 if args and args[0]:
734 if args and args[0]:
735 node = args.pop(0)
735 node = args.pop(0)
736 req.form['node'] = [node]
736 req.form['node'] = [node]
737 if args:
737 if args:
738 req.form['file'] = args
738 req.form['file'] = args
739
739
740 if cmd == 'static':
740 if cmd == 'static':
741 req.form['file'] = req.form['node']
741 req.form['file'] = req.form['node']
742 elif cmd == 'archive':
742 elif cmd == 'archive':
743 fn = req.form['node'][0]
743 fn = req.form['node'][0]
744 for type_, spec in self.archive_specs.iteritems():
744 for type_, spec in self.archive_specs.iteritems():
745 ext = spec[2]
745 ext = spec[2]
746 if fn.endswith(ext):
746 if fn.endswith(ext):
747 req.form['node'] = [fn[:-len(ext)]]
747 req.form['node'] = [fn[:-len(ext)]]
748 req.form['type'] = [type_]
748 req.form['type'] = [type_]
749
749
750 def sessionvars(**map):
750 def sessionvars(**map):
751 fields = []
751 fields = []
752 if req.form.has_key('style'):
752 if req.form.has_key('style'):
753 style = req.form['style'][0]
753 style = req.form['style'][0]
754 if style != self.config('web', 'style', ''):
754 if style != self.config('web', 'style', ''):
755 fields.append(('style', style))
755 fields.append(('style', style))
756
756
757 separator = req.url[-1] == '?' and ';' or '?'
757 separator = req.url[-1] == '?' and ';' or '?'
758 for name, value in fields:
758 for name, value in fields:
759 yield dict(name=name, value=value, separator=separator)
759 yield dict(name=name, value=value, separator=separator)
760 separator = ';'
760 separator = ';'
761
761
762 self.refresh()
762 self.refresh()
763
763
764 expand_form(req.form)
764 expand_form(req.form)
765 rewrite_request(req)
765 rewrite_request(req)
766
766
767 style = self.config("web", "style", "")
767 style = self.config("web", "style", "")
768 if req.form.has_key('style'):
768 if req.form.has_key('style'):
769 style = req.form['style'][0]
769 style = req.form['style'][0]
770 mapfile = style_map(self.templatepath, style)
770 mapfile = style_map(self.templatepath, style)
771
771
772 port = req.env["SERVER_PORT"]
772 port = req.env["SERVER_PORT"]
773 port = port != "80" and (":" + port) or ""
773 port = port != "80" and (":" + port) or ""
774 urlbase = 'http://%s%s' % (req.env['SERVER_NAME'], port)
774 urlbase = 'http://%s%s' % (req.env['SERVER_NAME'], port)
775 staticurl = self.config("web", "staticurl") or req.url + 'static/'
775 staticurl = self.config("web", "staticurl") or req.url + 'static/'
776 if not staticurl.endswith('/'):
776 if not staticurl.endswith('/'):
777 staticurl += '/'
777 staticurl += '/'
778
778
779 if not self.reponame:
779 if not self.reponame:
780 self.reponame = (self.config("web", "name")
780 self.reponame = (self.config("web", "name")
781 or req.env.get('REPO_NAME')
781 or req.env.get('REPO_NAME')
782 or req.url.strip('/') or self.repo.root)
782 or req.url.strip('/') or self.repo.root)
783
783
784 self.t = templater.templater(mapfile, templater.common_filters,
784 self.t = templater.templater(mapfile, templater.common_filters,
785 defaults={"url": req.url,
785 defaults={"url": req.url,
786 "staticurl": staticurl,
786 "staticurl": staticurl,
787 "urlbase": urlbase,
787 "urlbase": urlbase,
788 "repo": self.reponame,
788 "repo": self.reponame,
789 "header": header,
789 "header": header,
790 "footer": footer,
790 "footer": footer,
791 "motd": motd,
791 "motd": motd,
792 "rawfileheader": rawfileheader,
792 "rawfileheader": rawfileheader,
793 "sessionvars": sessionvars
793 "sessionvars": sessionvars
794 })
794 })
795
795
796 if not req.form.has_key('cmd'):
796 if not req.form.has_key('cmd'):
797 req.form['cmd'] = [self.t.cache['default']]
797 req.form['cmd'] = [self.t.cache['default']]
798
798
799 cmd = req.form['cmd'][0]
799 cmd = req.form['cmd'][0]
800
800
801 method = getattr(self, 'do_' + cmd, None)
801 method = getattr(self, 'do_' + cmd, None)
802 if method:
802 if method:
803 try:
803 try:
804 method(req)
804 method(req)
805 except (hg.RepoError, revlog.RevlogError), inst:
805 except (hg.RepoError, revlog.RevlogError), inst:
806 req.write(self.t("error", error=str(inst)))
806 req.write(self.t("error", error=str(inst)))
807 else:
807 else:
808 req.write(self.t("error", error='No such method: ' + cmd))
808 req.write(self.t("error", error='No such method: ' + cmd))
809
809
810 def changectx(self, req):
810 def changectx(self, req):
811 if req.form.has_key('node'):
811 if req.form.has_key('node'):
812 changeid = req.form['node'][0]
812 changeid = req.form['node'][0]
813 elif req.form.has_key('manifest'):
813 elif req.form.has_key('manifest'):
814 changeid = req.form['manifest'][0]
814 changeid = req.form['manifest'][0]
815 else:
815 else:
816 changeid = self.repo.changelog.count() - 1
816 changeid = self.repo.changelog.count() - 1
817
817
818 try:
818 try:
819 ctx = self.repo.changectx(changeid)
819 ctx = self.repo.changectx(changeid)
820 except hg.RepoError:
820 except hg.RepoError:
821 man = self.repo.manifest
821 man = self.repo.manifest
822 mn = man.lookup(changeid)
822 mn = man.lookup(changeid)
823 ctx = self.repo.changectx(man.linkrev(mn))
823 ctx = self.repo.changectx(man.linkrev(mn))
824
824
825 return ctx
825 return ctx
826
826
827 def filectx(self, req):
827 def filectx(self, req):
828 path = self.cleanpath(req.form['file'][0])
828 path = self.cleanpath(req.form['file'][0])
829 if req.form.has_key('node'):
829 if req.form.has_key('node'):
830 changeid = req.form['node'][0]
830 changeid = req.form['node'][0]
831 else:
831 else:
832 changeid = req.form['filenode'][0]
832 changeid = req.form['filenode'][0]
833 try:
833 try:
834 ctx = self.repo.changectx(changeid)
834 ctx = self.repo.changectx(changeid)
835 fctx = ctx.filectx(path)
835 fctx = ctx.filectx(path)
836 except hg.RepoError:
836 except hg.RepoError:
837 fctx = self.repo.filectx(path, fileid=changeid)
837 fctx = self.repo.filectx(path, fileid=changeid)
838
838
839 return fctx
839 return fctx
840
840
841 def stripes(self, parity):
841 def stripes(self, parity):
842 "make horizontal stripes for easier reading"
842 "make horizontal stripes for easier reading"
843 if self.stripecount:
843 if self.stripecount:
844 return (1 + parity / self.stripecount) & 1
844 return (1 + parity / self.stripecount) & 1
845 else:
845 else:
846 return 0
846 return 0
847
847
848 def do_log(self, req):
848 def do_log(self, req):
849 if req.form.has_key('file') and req.form['file'][0]:
849 if req.form.has_key('file') and req.form['file'][0]:
850 self.do_filelog(req)
850 self.do_filelog(req)
851 else:
851 else:
852 self.do_changelog(req)
852 self.do_changelog(req)
853
853
854 def do_rev(self, req):
854 def do_rev(self, req):
855 self.do_changeset(req)
855 self.do_changeset(req)
856
856
857 def do_file(self, req):
857 def do_file(self, req):
858 path = self.cleanpath(req.form.get('file', [''])[0])
858 path = self.cleanpath(req.form.get('file', [''])[0])
859 if path:
859 if path:
860 try:
860 try:
861 req.write(self.filerevision(self.filectx(req)))
861 req.write(self.filerevision(self.filectx(req)))
862 return
862 return
863 except revlog.LookupError:
863 except revlog.LookupError:
864 pass
864 pass
865
865
866 req.write(self.manifest(self.changectx(req), path))
866 req.write(self.manifest(self.changectx(req), path))
867
867
868 def do_diff(self, req):
868 def do_diff(self, req):
869 self.do_filediff(req)
869 self.do_filediff(req)
870
870
871 def do_changelog(self, req, shortlog = False):
871 def do_changelog(self, req, shortlog = False):
872 if req.form.has_key('node'):
872 if req.form.has_key('node'):
873 ctx = self.changectx(req)
873 ctx = self.changectx(req)
874 else:
874 else:
875 if req.form.has_key('rev'):
875 if req.form.has_key('rev'):
876 hi = req.form['rev'][0]
876 hi = req.form['rev'][0]
877 else:
877 else:
878 hi = self.repo.changelog.count() - 1
878 hi = self.repo.changelog.count() - 1
879 try:
879 try:
880 ctx = self.repo.changectx(hi)
880 ctx = self.repo.changectx(hi)
881 except hg.RepoError:
881 except hg.RepoError:
882 req.write(self.search(hi)) # XXX redirect to 404 page?
882 req.write(self.search(hi)) # XXX redirect to 404 page?
883 return
883 return
884
884
885 req.write(self.changelog(ctx, shortlog = shortlog))
885 req.write(self.changelog(ctx, shortlog = shortlog))
886
886
887 def do_shortlog(self, req):
887 def do_shortlog(self, req):
888 self.do_changelog(req, shortlog = True)
888 self.do_changelog(req, shortlog = True)
889
889
890 def do_changeset(self, req):
890 def do_changeset(self, req):
891 req.write(self.changeset(self.changectx(req)))
891 req.write(self.changeset(self.changectx(req)))
892
892
893 def do_manifest(self, req):
893 def do_manifest(self, req):
894 req.write(self.manifest(self.changectx(req),
894 req.write(self.manifest(self.changectx(req),
895 self.cleanpath(req.form['path'][0])))
895 self.cleanpath(req.form['path'][0])))
896
896
897 def do_tags(self, req):
897 def do_tags(self, req):
898 req.write(self.tags())
898 req.write(self.tags())
899
899
900 def do_summary(self, req):
900 def do_summary(self, req):
901 req.write(self.summary())
901 req.write(self.summary())
902
902
903 def do_filediff(self, req):
903 def do_filediff(self, req):
904 req.write(self.filediff(self.filectx(req)))
904 req.write(self.filediff(self.filectx(req)))
905
905
906 def do_annotate(self, req):
906 def do_annotate(self, req):
907 req.write(self.fileannotate(self.filectx(req)))
907 req.write(self.fileannotate(self.filectx(req)))
908
908
909 def do_filelog(self, req):
909 def do_filelog(self, req):
910 req.write(self.filelog(self.filectx(req)))
910 req.write(self.filelog(self.filectx(req)))
911
911
912 def do_lookup(self, req):
912 def do_lookup(self, req):
913 try:
913 try:
914 r = hex(self.repo.lookup(req.form['key'][0]))
914 r = hex(self.repo.lookup(req.form['key'][0]))
915 success = 1
915 success = 1
916 except Exception,inst:
916 except Exception,inst:
917 r = str(inst)
917 r = str(inst)
918 success = 0
918 success = 0
919 resp = "%s %s\n" % (success, r)
919 resp = "%s %s\n" % (success, r)
920 req.httphdr("application/mercurial-0.1", length=len(resp))
920 req.httphdr("application/mercurial-0.1", length=len(resp))
921 req.write(resp)
921 req.write(resp)
922
922
923 def do_heads(self, req):
923 def do_heads(self, req):
924 resp = " ".join(map(hex, self.repo.heads())) + "\n"
924 resp = " ".join(map(hex, self.repo.heads())) + "\n"
925 req.httphdr("application/mercurial-0.1", length=len(resp))
925 req.httphdr("application/mercurial-0.1", length=len(resp))
926 req.write(resp)
926 req.write(resp)
927
927
928 def do_branches(self, req):
928 def do_branches(self, req):
929 nodes = []
929 nodes = []
930 if req.form.has_key('nodes'):
930 if req.form.has_key('nodes'):
931 nodes = map(bin, req.form['nodes'][0].split(" "))
931 nodes = map(bin, req.form['nodes'][0].split(" "))
932 resp = cStringIO.StringIO()
932 resp = cStringIO.StringIO()
933 for b in self.repo.branches(nodes):
933 for b in self.repo.branches(nodes):
934 resp.write(" ".join(map(hex, b)) + "\n")
934 resp.write(" ".join(map(hex, b)) + "\n")
935 resp = resp.getvalue()
935 resp = resp.getvalue()
936 req.httphdr("application/mercurial-0.1", length=len(resp))
936 req.httphdr("application/mercurial-0.1", length=len(resp))
937 req.write(resp)
937 req.write(resp)
938
938
939 def do_between(self, req):
939 def do_between(self, req):
940 if req.form.has_key('pairs'):
940 if req.form.has_key('pairs'):
941 pairs = [map(bin, p.split("-"))
941 pairs = [map(bin, p.split("-"))
942 for p in req.form['pairs'][0].split(" ")]
942 for p in req.form['pairs'][0].split(" ")]
943 resp = cStringIO.StringIO()
943 resp = cStringIO.StringIO()
944 for b in self.repo.between(pairs):
944 for b in self.repo.between(pairs):
945 resp.write(" ".join(map(hex, b)) + "\n")
945 resp.write(" ".join(map(hex, b)) + "\n")
946 resp = resp.getvalue()
946 resp = resp.getvalue()
947 req.httphdr("application/mercurial-0.1", length=len(resp))
947 req.httphdr("application/mercurial-0.1", length=len(resp))
948 req.write(resp)
948 req.write(resp)
949
949
950 def do_changegroup(self, req):
950 def do_changegroup(self, req):
951 req.httphdr("application/mercurial-0.1")
951 req.httphdr("application/mercurial-0.1")
952 nodes = []
952 nodes = []
953 if not self.allowpull:
953 if not self.allowpull:
954 return
954 return
955
955
956 if req.form.has_key('roots'):
956 if req.form.has_key('roots'):
957 nodes = map(bin, req.form['roots'][0].split(" "))
957 nodes = map(bin, req.form['roots'][0].split(" "))
958
958
959 z = zlib.compressobj()
959 z = zlib.compressobj()
960 f = self.repo.changegroup(nodes, 'serve')
960 f = self.repo.changegroup(nodes, 'serve')
961 while 1:
961 while 1:
962 chunk = f.read(4096)
962 chunk = f.read(4096)
963 if not chunk:
963 if not chunk:
964 break
964 break
965 req.write(z.compress(chunk))
965 req.write(z.compress(chunk))
966
966
967 req.write(z.flush())
967 req.write(z.flush())
968
968
969 def do_changegroupsubset(self, req):
969 def do_changegroupsubset(self, req):
970 req.httphdr("application/mercurial-0.1")
970 req.httphdr("application/mercurial-0.1")
971 bases = []
971 bases = []
972 heads = []
972 heads = []
973 if not self.allowpull:
973 if not self.allowpull:
974 return
974 return
975
975
976 if req.form.has_key('bases'):
976 if req.form.has_key('bases'):
977 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
977 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
978 if req.form.has_key('heads'):
978 if req.form.has_key('heads'):
979 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
979 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
980
980
981 z = zlib.compressobj()
981 z = zlib.compressobj()
982 f = self.repo.changegroupsubset(bases, heads, 'serve')
982 f = self.repo.changegroupsubset(bases, heads, 'serve')
983 while 1:
983 while 1:
984 chunk = f.read(4096)
984 chunk = f.read(4096)
985 if not chunk:
985 if not chunk:
986 break
986 break
987 req.write(z.compress(chunk))
987 req.write(z.compress(chunk))
988
988
989 req.write(z.flush())
989 req.write(z.flush())
990
990
991 def do_archive(self, req):
991 def do_archive(self, req):
992 changeset = self.repo.lookup(req.form['node'][0])
992 changeset = self.repo.lookup(req.form['node'][0])
993 type_ = req.form['type'][0]
993 type_ = req.form['type'][0]
994 allowed = self.configlist("web", "allow_archive")
994 allowed = self.configlist("web", "allow_archive")
995 if (type_ in self.archives and (type_ in allowed or
995 if (type_ in self.archives and (type_ in allowed or
996 self.configbool("web", "allow" + type_, False))):
996 self.configbool("web", "allow" + type_, False))):
997 self.archive(req, changeset, type_)
997 self.archive(req, changeset, type_)
998 return
998 return
999
999
1000 req.write(self.t("error"))
1000 req.write(self.t("error"))
1001
1001
1002 def do_static(self, req):
1002 def do_static(self, req):
1003 fname = req.form['file'][0]
1003 fname = req.form['file'][0]
1004 # a repo owner may set web.static in .hg/hgrc to get any file
1004 # a repo owner may set web.static in .hg/hgrc to get any file
1005 # readable by the user running the CGI script
1005 # readable by the user running the CGI script
1006 static = self.config("web", "static",
1006 static = self.config("web", "static",
1007 os.path.join(self.templatepath, "static"),
1007 os.path.join(self.templatepath, "static"),
1008 untrusted=False)
1008 untrusted=False)
1009 req.write(staticfile(static, fname, req)
1009 req.write(staticfile(static, fname, req)
1010 or self.t("error", error="%r not found" % fname))
1010 or self.t("error", error="%r not found" % fname))
1011
1011
1012 def do_capabilities(self, req):
1012 def do_capabilities(self, req):
1013 caps = ['lookup', 'changegroupsubset']
1013 caps = ['lookup', 'changegroupsubset']
1014 if self.configbool('server', 'uncompressed'):
1014 if self.configbool('server', 'uncompressed'):
1015 caps.append('stream=%d' % self.repo.revlogversion)
1015 caps.append('stream=%d' % self.repo.revlogversion)
1016 # XXX: make configurable and/or share code with do_unbundle:
1016 # XXX: make configurable and/or share code with do_unbundle:
1017 unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
1017 unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
1018 if unbundleversions:
1018 if unbundleversions:
1019 caps.append('unbundle=%s' % ','.join(unbundleversions))
1019 caps.append('unbundle=%s' % ','.join(unbundleversions))
1020 resp = ' '.join(caps)
1020 resp = ' '.join(caps)
1021 req.httphdr("application/mercurial-0.1", length=len(resp))
1021 req.httphdr("application/mercurial-0.1", length=len(resp))
1022 req.write(resp)
1022 req.write(resp)
1023
1023
1024 def check_perm(self, req, op, default):
1024 def check_perm(self, req, op, default):
1025 '''check permission for operation based on user auth.
1025 '''check permission for operation based on user auth.
1026 return true if op allowed, else false.
1026 return true if op allowed, else false.
1027 default is policy to use if no config given.'''
1027 default is policy to use if no config given.'''
1028
1028
1029 user = req.env.get('REMOTE_USER')
1029 user = req.env.get('REMOTE_USER')
1030
1030
1031 deny = self.configlist('web', 'deny_' + op)
1031 deny = self.configlist('web', 'deny_' + op)
1032 if deny and (not user or deny == ['*'] or user in deny):
1032 if deny and (not user or deny == ['*'] or user in deny):
1033 return False
1033 return False
1034
1034
1035 allow = self.configlist('web', 'allow_' + op)
1035 allow = self.configlist('web', 'allow_' + op)
1036 return (allow and (allow == ['*'] or user in allow)) or default
1036 return (allow and (allow == ['*'] or user in allow)) or default
1037
1037
1038 def do_unbundle(self, req):
1038 def do_unbundle(self, req):
1039 def bail(response, headers={}):
1039 def bail(response, headers={}):
1040 length = int(req.env['CONTENT_LENGTH'])
1040 length = int(req.env['CONTENT_LENGTH'])
1041 for s in util.filechunkiter(req, limit=length):
1041 for s in util.filechunkiter(req, limit=length):
1042 # drain incoming bundle, else client will not see
1042 # drain incoming bundle, else client will not see
1043 # response when run outside cgi script
1043 # response when run outside cgi script
1044 pass
1044 pass
1045 req.httphdr("application/mercurial-0.1", headers=headers)
1045 req.httphdr("application/mercurial-0.1", headers=headers)
1046 req.write('0\n')
1046 req.write('0\n')
1047 req.write(response)
1047 req.write(response)
1048
1048
1049 # require ssl by default, auth info cannot be sniffed and
1049 # require ssl by default, auth info cannot be sniffed and
1050 # replayed
1050 # replayed
1051 ssl_req = self.configbool('web', 'push_ssl', True)
1051 ssl_req = self.configbool('web', 'push_ssl', True)
1052 if ssl_req:
1052 if ssl_req:
1053 if not req.env.get('HTTPS'):
1053 if not req.env.get('HTTPS'):
1054 bail(_('ssl required\n'))
1054 bail(_('ssl required\n'))
1055 return
1055 return
1056 proto = 'https'
1056 proto = 'https'
1057 else:
1057 else:
1058 proto = 'http'
1058 proto = 'http'
1059
1059
1060 # do not allow push unless explicitly allowed
1060 # do not allow push unless explicitly allowed
1061 if not self.check_perm(req, 'push', False):
1061 if not self.check_perm(req, 'push', False):
1062 bail(_('push not authorized\n'),
1062 bail(_('push not authorized\n'),
1063 headers={'status': '401 Unauthorized'})
1063 headers={'status': '401 Unauthorized'})
1064 return
1064 return
1065
1065
1066 req.httphdr("application/mercurial-0.1")
1066 req.httphdr("application/mercurial-0.1")
1067
1067
1068 their_heads = req.form['heads'][0].split(' ')
1068 their_heads = req.form['heads'][0].split(' ')
1069
1069
1070 def check_heads():
1070 def check_heads():
1071 heads = map(hex, self.repo.heads())
1071 heads = map(hex, self.repo.heads())
1072 return their_heads == [hex('force')] or their_heads == heads
1072 return their_heads == [hex('force')] or their_heads == heads
1073
1073
1074 # fail early if possible
1074 # fail early if possible
1075 if not check_heads():
1075 if not check_heads():
1076 bail(_('unsynced changes\n'))
1076 bail(_('unsynced changes\n'))
1077 return
1077 return
1078
1078
1079 # do not lock repo until all changegroup data is
1079 # do not lock repo until all changegroup data is
1080 # streamed. save to temporary file.
1080 # streamed. save to temporary file.
1081
1081
1082 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1082 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1083 fp = os.fdopen(fd, 'wb+')
1083 fp = os.fdopen(fd, 'wb+')
1084 try:
1084 try:
1085 length = int(req.env['CONTENT_LENGTH'])
1085 length = int(req.env['CONTENT_LENGTH'])
1086 for s in util.filechunkiter(req, limit=length):
1086 for s in util.filechunkiter(req, limit=length):
1087 fp.write(s)
1087 fp.write(s)
1088
1088
1089 lock = self.repo.lock()
1089 lock = self.repo.lock()
1090 try:
1090 try:
1091 if not check_heads():
1091 if not check_heads():
1092 req.write('0\n')
1092 req.write('0\n')
1093 req.write(_('unsynced changes\n'))
1093 req.write(_('unsynced changes\n'))
1094 return
1094 return
1095
1095
1096 fp.seek(0)
1096 fp.seek(0)
1097 header = fp.read(6)
1097 header = fp.read(6)
1098 if not header.startswith("HG"):
1098 if not header.startswith("HG"):
1099 # old client with uncompressed bundle
1099 # old client with uncompressed bundle
1100 def generator(f):
1100 def generator(f):
1101 yield header
1101 yield header
1102 for chunk in f:
1102 for chunk in f:
1103 yield chunk
1103 yield chunk
1104 elif not header.startswith("HG10"):
1104 elif not header.startswith("HG10"):
1105 req.write("0\n")
1105 req.write("0\n")
1106 req.write(_("unknown bundle version\n"))
1106 req.write(_("unknown bundle version\n"))
1107 return
1107 return
1108 elif header == "HG10GZ":
1108 elif header == "HG10GZ":
1109 def generator(f):
1109 def generator(f):
1110 zd = zlib.decompressobj()
1110 zd = zlib.decompressobj()
1111 for chunk in f:
1111 for chunk in f:
1112 yield zd.decompress(chunk)
1112 yield zd.decompress(chunk)
1113 elif header == "HG10BZ":
1113 elif header == "HG10BZ":
1114 def generator(f):
1114 def generator(f):
1115 zd = bz2.BZ2Decompressor()
1115 zd = bz2.BZ2Decompressor()
1116 zd.decompress("BZ")
1116 zd.decompress("BZ")
1117 for chunk in f:
1117 for chunk in f:
1118 yield zd.decompress(chunk)
1118 yield zd.decompress(chunk)
1119 elif header == "HG10UN":
1119 elif header == "HG10UN":
1120 def generator(f):
1120 def generator(f):
1121 for chunk in f:
1121 for chunk in f:
1122 yield chunk
1122 yield chunk
1123 else:
1123 else:
1124 req.write("0\n")
1124 req.write("0\n")
1125 req.write(_("unknown bundle compression type\n"))
1125 req.write(_("unknown bundle compression type\n"))
1126 return
1126 return
1127 gen = generator(util.filechunkiter(fp, 4096))
1127 gen = generator(util.filechunkiter(fp, 4096))
1128
1128
1129 # send addchangegroup output to client
1129 # send addchangegroup output to client
1130
1130
1131 old_stdout = sys.stdout
1131 old_stdout = sys.stdout
1132 sys.stdout = cStringIO.StringIO()
1132 sys.stdout = cStringIO.StringIO()
1133
1133
1134 try:
1134 try:
1135 url = 'remote:%s:%s' % (proto,
1135 url = 'remote:%s:%s' % (proto,
1136 req.env.get('REMOTE_HOST', ''))
1136 req.env.get('REMOTE_HOST', ''))
1137 try:
1137 ret = self.repo.addchangegroup(util.chunkbuffer(gen),
1138 ret = self.repo.addchangegroup(util.chunkbuffer(gen),
1138 'serve', url)
1139 'serve', url)
1140 except util.Abort, inst:
1141 sys.stdout.write("abort: %s\n" % inst)
1142 ret = 0
1139 finally:
1143 finally:
1140 val = sys.stdout.getvalue()
1144 val = sys.stdout.getvalue()
1141 sys.stdout = old_stdout
1145 sys.stdout = old_stdout
1142 req.write('%d\n' % ret)
1146 req.write('%d\n' % ret)
1143 req.write(val)
1147 req.write(val)
1144 finally:
1148 finally:
1145 lock.release()
1149 lock.release()
1146 finally:
1150 finally:
1147 fp.close()
1151 fp.close()
1148 os.unlink(tempname)
1152 os.unlink(tempname)
1149
1153
1150 def do_stream_out(self, req):
1154 def do_stream_out(self, req):
1151 req.httphdr("application/mercurial-0.1")
1155 req.httphdr("application/mercurial-0.1")
1152 streamclone.stream_out(self.repo, req)
1156 streamclone.stream_out(self.repo, req)
@@ -1,246 +1,246 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
9 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
10 from mercurial import ui, hg, util, templater
10 from mercurial import ui, hg, util, templater
11 from hgweb_mod import hgweb
11 from hgweb_mod import hgweb
12 from hgwebdir_mod import hgwebdir
12 from hgwebdir_mod import hgwebdir
13 from request import wsgiapplication
13 from request import wsgiapplication
14 from mercurial.i18n import gettext as _
14 from mercurial.i18n import gettext as _
15
15
16 def _splitURI(uri):
16 def _splitURI(uri):
17 """ Return path and query splited from uri
17 """ Return path and query splited from uri
18
18
19 Just like CGI environment, the path is unquoted, the query is
19 Just like CGI environment, the path is unquoted, the query is
20 not.
20 not.
21 """
21 """
22 if '?' in uri:
22 if '?' in uri:
23 path, query = uri.split('?', 1)
23 path, query = uri.split('?', 1)
24 else:
24 else:
25 path, query = uri, ''
25 path, query = uri, ''
26 return urllib.unquote(path), query
26 return urllib.unquote(path), query
27
27
28 class _error_logger(object):
28 class _error_logger(object):
29 def __init__(self, handler):
29 def __init__(self, handler):
30 self.handler = handler
30 self.handler = handler
31 def flush(self):
31 def flush(self):
32 pass
32 pass
33 def write(self, str):
33 def write(self, str):
34 self.writelines(str.split('\n'))
34 self.writelines(str.split('\n'))
35 def writelines(self, seq):
35 def writelines(self, seq):
36 for msg in seq:
36 for msg in seq:
37 self.handler.log_error("HG error: %s", msg)
37 self.handler.log_error("HG error: %s", msg)
38
38
39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
40 def __init__(self, *args, **kargs):
40 def __init__(self, *args, **kargs):
41 self.protocol_version = 'HTTP/1.1'
41 self.protocol_version = 'HTTP/1.1'
42 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
42 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
43
43
44 def log_error(self, format, *args):
44 def log_error(self, format, *args):
45 errorlog = self.server.errorlog
45 errorlog = self.server.errorlog
46 errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
46 errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
47 self.log_date_time_string(),
47 self.log_date_time_string(),
48 format % args))
48 format % args))
49
49
50 def log_message(self, format, *args):
50 def log_message(self, format, *args):
51 accesslog = self.server.accesslog
51 accesslog = self.server.accesslog
52 accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
52 accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
53 self.log_date_time_string(),
53 self.log_date_time_string(),
54 format % args))
54 format % args))
55
55
56 def do_POST(self):
56 def do_POST(self):
57 try:
57 try:
58 try:
58 try:
59 self.do_hgweb()
59 self.do_hgweb()
60 except socket.error, inst:
60 except socket.error, inst:
61 if inst[0] != errno.EPIPE:
61 if inst[0] != errno.EPIPE:
62 raise
62 raise
63 except StandardError, inst:
63 except StandardError, inst:
64 self._start_response("500 Internal Server Error", [])
64 self._start_response("500 Internal Server Error", [])
65 self._write("Internal Server Error")
65 self._write("Internal Server Error")
66 tb = "".join(traceback.format_exception(*sys.exc_info()))
66 tb = "".join(traceback.format_exception(*sys.exc_info()))
67 self.log_error("Exception happened during processing request '%s':\n%s",
67 self.log_error("Exception happened during processing request '%s':\n%s",
68 self.path, tb)
68 self.path, tb)
69
69
70 def do_GET(self):
70 def do_GET(self):
71 self.do_POST()
71 self.do_POST()
72
72
73 def do_hgweb(self):
73 def do_hgweb(self):
74 path_info, query = _splitURI(self.path)
74 path_info, query = _splitURI(self.path)
75
75
76 env = {}
76 env = {}
77 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
77 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
78 env['REQUEST_METHOD'] = self.command
78 env['REQUEST_METHOD'] = self.command
79 env['SERVER_NAME'] = self.server.server_name
79 env['SERVER_NAME'] = self.server.server_name
80 env['SERVER_PORT'] = str(self.server.server_port)
80 env['SERVER_PORT'] = str(self.server.server_port)
81 env['REQUEST_URI'] = self.path
81 env['REQUEST_URI'] = self.path
82 env['PATH_INFO'] = path_info
82 env['PATH_INFO'] = path_info
83 if query:
83 if query:
84 env['QUERY_STRING'] = query
84 env['QUERY_STRING'] = query
85 host = self.address_string()
85 host = self.address_string()
86 if host != self.client_address[0]:
86 if host != self.client_address[0]:
87 env['REMOTE_HOST'] = host
87 env['REMOTE_HOST'] = host
88 env['REMOTE_ADDR'] = self.client_address[0]
88 env['REMOTE_ADDR'] = self.client_address[0]
89
89
90 if self.headers.typeheader is None:
90 if self.headers.typeheader is None:
91 env['CONTENT_TYPE'] = self.headers.type
91 env['CONTENT_TYPE'] = self.headers.type
92 else:
92 else:
93 env['CONTENT_TYPE'] = self.headers.typeheader
93 env['CONTENT_TYPE'] = self.headers.typeheader
94 length = self.headers.getheader('content-length')
94 length = self.headers.getheader('content-length')
95 if length:
95 if length:
96 env['CONTENT_LENGTH'] = length
96 env['CONTENT_LENGTH'] = length
97 for header in [h for h in self.headers.keys() \
97 for header in [h for h in self.headers.keys() \
98 if h not in ('content-type', 'content-length')]:
98 if h not in ('content-type', 'content-length')]:
99 hkey = 'HTTP_' + header.replace('-', '_').upper()
99 hkey = 'HTTP_' + header.replace('-', '_').upper()
100 hval = self.headers.getheader(header)
100 hval = self.headers.getheader(header)
101 hval = hval.replace('\n', '').strip()
101 hval = hval.replace('\n', '').strip()
102 if hval:
102 if hval:
103 env[hkey] = hval
103 env[hkey] = hval
104 env['SERVER_PROTOCOL'] = self.request_version
104 env['SERVER_PROTOCOL'] = self.request_version
105 env['wsgi.version'] = (1, 0)
105 env['wsgi.version'] = (1, 0)
106 env['wsgi.url_scheme'] = 'http'
106 env['wsgi.url_scheme'] = 'http'
107 env['wsgi.input'] = self.rfile
107 env['wsgi.input'] = self.rfile
108 env['wsgi.errors'] = _error_logger(self)
108 env['wsgi.errors'] = _error_logger(self)
109 env['wsgi.multithread'] = isinstance(self.server,
109 env['wsgi.multithread'] = isinstance(self.server,
110 SocketServer.ThreadingMixIn)
110 SocketServer.ThreadingMixIn)
111 env['wsgi.multiprocess'] = isinstance(self.server,
111 env['wsgi.multiprocess'] = isinstance(self.server,
112 SocketServer.ForkingMixIn)
112 SocketServer.ForkingMixIn)
113 env['wsgi.run_once'] = 0
113 env['wsgi.run_once'] = 0
114
114
115 self.close_connection = True
115 self.close_connection = True
116 self.saved_status = None
116 self.saved_status = None
117 self.saved_headers = []
117 self.saved_headers = []
118 self.sent_headers = False
118 self.sent_headers = False
119 self.length = None
119 self.length = None
120 req = self.server.reqmaker(env, self._start_response)
120 req = self.server.reqmaker(env, self._start_response)
121 for data in req:
121 for data in req:
122 if data:
122 if data:
123 self._write(data)
123 self._write(data)
124
124
125 def send_headers(self):
125 def send_headers(self):
126 if not self.saved_status:
126 if not self.saved_status:
127 raise AssertionError("Sending headers before start_response() called")
127 raise AssertionError("Sending headers before start_response() called")
128 saved_status = self.saved_status.split(None, 1)
128 saved_status = self.saved_status.split(None, 1)
129 saved_status[0] = int(saved_status[0])
129 saved_status[0] = int(saved_status[0])
130 self.send_response(*saved_status)
130 self.send_response(*saved_status)
131 should_close = True
131 should_close = True
132 for h in self.saved_headers:
132 for h in self.saved_headers:
133 self.send_header(*h)
133 self.send_header(*h)
134 if h[0].lower() == 'content-length':
134 if h[0].lower() == 'content-length':
135 should_close = False
135 should_close = False
136 self.length = int(h[1])
136 self.length = int(h[1])
137 # The value of the Connection header is a list of case-insensitive
137 # The value of the Connection header is a list of case-insensitive
138 # tokens separated by commas and optional whitespace.
138 # tokens separated by commas and optional whitespace.
139 if 'close' in [token.strip().lower() for token in
139 if 'close' in [token.strip().lower() for token in
140 self.headers.get('connection', '').split(',')]:
140 self.headers.get('connection', '').split(',')]:
141 should_close = True
141 should_close = True
142 if should_close:
142 if should_close:
143 self.send_header('Connection', 'close')
143 self.send_header('Connection', 'close')
144 self.close_connection = should_close
144 self.close_connection = should_close
145 self.end_headers()
145 self.end_headers()
146 self.sent_headers = True
146 self.sent_headers = True
147
147
148 def _start_response(self, http_status, headers, exc_info=None):
148 def _start_response(self, http_status, headers, exc_info=None):
149 code, msg = http_status.split(None, 1)
149 code, msg = http_status.split(None, 1)
150 code = int(code)
150 code = int(code)
151 self.saved_status = http_status
151 self.saved_status = http_status
152 bad_headers = ('connection', 'transfer-encoding')
152 bad_headers = ('connection', 'transfer-encoding')
153 self.saved_headers = [ h for h in headers \
153 self.saved_headers = [ h for h in headers \
154 if h[0].lower() not in bad_headers ]
154 if h[0].lower() not in bad_headers ]
155 return self._write
155 return self._write
156
156
157 def _write(self, data):
157 def _write(self, data):
158 if not self.saved_status:
158 if not self.saved_status:
159 raise AssertionError("data written before start_response() called")
159 raise AssertionError("data written before start_response() called")
160 elif not self.sent_headers:
160 elif not self.sent_headers:
161 self.send_headers()
161 self.send_headers()
162 if self.length is not None:
162 if self.length is not None:
163 if len(data) > self.length:
163 if len(data) > self.length:
164 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
164 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
165 self.length = self.length - len(data)
165 self.length = self.length - len(data)
166 self.wfile.write(data)
166 self.wfile.write(data)
167 self.wfile.flush()
167 self.wfile.flush()
168
168
169 def create_server(ui, repo):
169 def create_server(ui, repo):
170 use_threads = True
170 use_threads = True
171
171
172 def openlog(opt, default):
172 def openlog(opt, default):
173 if opt and opt != '-':
173 if opt and opt != '-':
174 return open(opt, 'w')
174 return open(opt, 'w')
175 return default
175 return default
176
176
177 address = ui.config("web", "address", "")
177 address = ui.config("web", "address", "")
178 port = int(ui.config("web", "port", 8000))
178 port = int(ui.config("web", "port", 8000))
179 use_ipv6 = ui.configbool("web", "ipv6")
179 use_ipv6 = ui.configbool("web", "ipv6")
180 webdir_conf = ui.config("web", "webdir_conf")
180 webdir_conf = ui.config("web", "webdir_conf")
181 accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
181 accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
182 errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
182 errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
183
183
184 if use_threads:
184 if use_threads:
185 try:
185 try:
186 from threading import activeCount
186 from threading import activeCount
187 except ImportError:
187 except ImportError:
188 use_threads = False
188 use_threads = False
189
189
190 if use_threads:
190 if use_threads:
191 _mixin = SocketServer.ThreadingMixIn
191 _mixin = SocketServer.ThreadingMixIn
192 else:
192 else:
193 if hasattr(os, "fork"):
193 if hasattr(os, "fork"):
194 _mixin = SocketServer.ForkingMixIn
194 _mixin = SocketServer.ForkingMixIn
195 else:
195 else:
196 class _mixin:
196 class _mixin:
197 pass
197 pass
198
198
199 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
199 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
200 def __init__(self, *args, **kargs):
200 def __init__(self, *args, **kargs):
201 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
201 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
202 self.accesslog = accesslog
202 self.accesslog = accesslog
203 self.errorlog = errorlog
203 self.errorlog = errorlog
204 self.repo = repo
204 self.repo = repo
205 self.webdir_conf = webdir_conf
205 self.webdir_conf = webdir_conf
206 self.webdirmaker = hgwebdir
206 self.webdirmaker = hgwebdir
207 self.repoviewmaker = hgweb
207 self.repoviewmaker = hgweb
208 self.reqmaker = wsgiapplication(self.make_handler)
208 self.reqmaker = wsgiapplication(self.make_handler)
209 self.daemon_threads = True
209 self.daemon_threads = True
210
210
211 addr, port = self.socket.getsockname()[:2]
211 addr, port = self.socket.getsockname()[:2]
212 if addr in ('0.0.0.0', '::'):
212 if addr in ('0.0.0.0', '::'):
213 addr = socket.gethostname()
213 addr = socket.gethostname()
214 else:
214 else:
215 try:
215 try:
216 addr = socket.gethostbyaddr(addr)[0]
216 addr = socket.gethostbyaddr(addr)[0]
217 except socket.error:
217 except socket.error:
218 pass
218 pass
219 self.addr, self.port = addr, port
219 self.addr, self.port = addr, port
220
220
221 def make_handler(self):
221 def make_handler(self):
222 if self.webdir_conf:
222 if self.webdir_conf:
223 hgwebobj = self.webdirmaker(self.webdir_conf, ui)
223 hgwebobj = self.webdirmaker(self.webdir_conf, ui)
224 elif self.repo is not None:
224 elif self.repo is not None:
225 hgwebobj = self.repoviewmaker(repo.__class__(repo.ui,
225 hgwebobj = self.repoviewmaker(hg.repository(repo.ui,
226 repo.origroot))
226 repo.root))
227 else:
227 else:
228 raise hg.RepoError(_("There is no Mercurial repository here"
228 raise hg.RepoError(_("There is no Mercurial repository here"
229 " (.hg not found)"))
229 " (.hg not found)"))
230 return hgwebobj
230 return hgwebobj
231
231
232 class IPv6HTTPServer(MercurialHTTPServer):
232 class IPv6HTTPServer(MercurialHTTPServer):
233 address_family = getattr(socket, 'AF_INET6', None)
233 address_family = getattr(socket, 'AF_INET6', None)
234
234
235 def __init__(self, *args, **kwargs):
235 def __init__(self, *args, **kwargs):
236 if self.address_family is None:
236 if self.address_family is None:
237 raise hg.RepoError(_('IPv6 not available on this system'))
237 raise hg.RepoError(_('IPv6 not available on this system'))
238 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
238 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
239
239
240 try:
240 try:
241 if use_ipv6:
241 if use_ipv6:
242 return IPv6HTTPServer((address, port), _hgwebhandler)
242 return IPv6HTTPServer((address, port), _hgwebhandler)
243 else:
243 else:
244 return MercurialHTTPServer((address, port), _hgwebhandler)
244 return MercurialHTTPServer((address, port), _hgwebhandler)
245 except socket.error, inst:
245 except socket.error, inst:
246 raise util.Abort(_('cannot start server: %s') % inst.args[1])
246 raise util.Abort(_('cannot start server: %s') % inst.args[1])
@@ -1,67 +1,70 b''
1 # mail.py - mail sending bits for mercurial
1 # mail.py - mail sending bits for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 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
7
8 from i18n import _
8 from i18n import _
9 import os, smtplib, templater, util
9 import os, smtplib, templater, util, socket
10
10
11 def _smtp(ui):
11 def _smtp(ui):
12 '''send mail using smtp.'''
12 '''send mail using smtp.'''
13
13
14 local_hostname = ui.config('smtp', 'local_hostname')
14 local_hostname = ui.config('smtp', 'local_hostname')
15 s = smtplib.SMTP(local_hostname=local_hostname)
15 s = smtplib.SMTP(local_hostname=local_hostname)
16 mailhost = ui.config('smtp', 'host')
16 mailhost = ui.config('smtp', 'host')
17 if not mailhost:
17 if not mailhost:
18 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
18 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
19 mailport = int(ui.config('smtp', 'port', 25))
19 mailport = int(ui.config('smtp', 'port', 25))
20 ui.note(_('sending mail: smtp host %s, port %s\n') %
20 ui.note(_('sending mail: smtp host %s, port %s\n') %
21 (mailhost, mailport))
21 (mailhost, mailport))
22 s.connect(host=mailhost, port=mailport)
22 s.connect(host=mailhost, port=mailport)
23 if ui.configbool('smtp', 'tls'):
23 if ui.configbool('smtp', 'tls'):
24 if not hasattr(socket, 'ssl'):
25 raise util.Abort(_("can't use TLS: Python SSL support "
26 "not installed"))
24 ui.note(_('(using tls)\n'))
27 ui.note(_('(using tls)\n'))
25 s.ehlo()
28 s.ehlo()
26 s.starttls()
29 s.starttls()
27 s.ehlo()
30 s.ehlo()
28 username = ui.config('smtp', 'username')
31 username = ui.config('smtp', 'username')
29 password = ui.config('smtp', 'password')
32 password = ui.config('smtp', 'password')
30 if username and password:
33 if username and password:
31 ui.note(_('(authenticating to mail server as %s)\n') %
34 ui.note(_('(authenticating to mail server as %s)\n') %
32 (username))
35 (username))
33 s.login(username, password)
36 s.login(username, password)
34 return s
37 return s
35
38
36 class _sendmail(object):
39 class _sendmail(object):
37 '''send mail using sendmail.'''
40 '''send mail using sendmail.'''
38
41
39 def __init__(self, ui, program):
42 def __init__(self, ui, program):
40 self.ui = ui
43 self.ui = ui
41 self.program = program
44 self.program = program
42
45
43 def sendmail(self, sender, recipients, msg):
46 def sendmail(self, sender, recipients, msg):
44 cmdline = '%s -f %s %s' % (
47 cmdline = '%s -f %s %s' % (
45 self.program, templater.email(sender),
48 self.program, templater.email(sender),
46 ' '.join(map(templater.email, recipients)))
49 ' '.join(map(templater.email, recipients)))
47 self.ui.note(_('sending mail: %s\n') % cmdline)
50 self.ui.note(_('sending mail: %s\n') % cmdline)
48 fp = os.popen(cmdline, 'w')
51 fp = os.popen(cmdline, 'w')
49 fp.write(msg)
52 fp.write(msg)
50 ret = fp.close()
53 ret = fp.close()
51 if ret:
54 if ret:
52 raise util.Abort('%s %s' % (
55 raise util.Abort('%s %s' % (
53 os.path.basename(self.program.split(None, 1)[0]),
56 os.path.basename(self.program.split(None, 1)[0]),
54 util.explain_exit(ret)[0]))
57 util.explain_exit(ret)[0]))
55
58
56 def connect(ui):
59 def connect(ui):
57 '''make a mail connection. object returned has one method, sendmail.
60 '''make a mail connection. object returned has one method, sendmail.
58 call as sendmail(sender, list-of-recipients, msg).'''
61 call as sendmail(sender, list-of-recipients, msg).'''
59
62
60 method = ui.config('email', 'method', 'smtp')
63 method = ui.config('email', 'method', 'smtp')
61 if method == 'smtp':
64 if method == 'smtp':
62 return _smtp(ui)
65 return _smtp(ui)
63
66
64 return _sendmail(ui, method)
67 return _sendmail(ui, method)
65
68
66 def sendmail(ui, sender, recipients, msg):
69 def sendmail(ui, sender, recipients, msg):
67 return connect(ui).sendmail(sender, recipients, msg)
70 return connect(ui).sendmail(sender, recipients, msg)
@@ -1,648 +1,650 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 from node import *
9 from node import *
10 import base85, cmdutil, mdiff, util, context, revlog
10 import base85, cmdutil, mdiff, util, context, revlog
11 import cStringIO, email.Parser, os, popen2, re, sha
11 import cStringIO, email.Parser, os, popen2, re, sha
12 import sys, tempfile, zlib
12 import sys, tempfile, zlib
13
13
14 # helper functions
14 # helper functions
15
15
16 def copyfile(src, dst, basedir=None):
16 def copyfile(src, dst, basedir=None):
17 if not basedir:
17 if not basedir:
18 basedir = os.getcwd()
18 basedir = os.getcwd()
19
19
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 if os.path.exists(absdst):
21 if os.path.exists(absdst):
22 raise util.Abort(_("cannot create %s: destination already exists") %
22 raise util.Abort(_("cannot create %s: destination already exists") %
23 dst)
23 dst)
24
24
25 targetdir = os.path.dirname(absdst)
25 targetdir = os.path.dirname(absdst)
26 if not os.path.isdir(targetdir):
26 if not os.path.isdir(targetdir):
27 os.makedirs(targetdir)
27 os.makedirs(targetdir)
28
28
29 util.copyfile(abssrc, absdst)
29 util.copyfile(abssrc, absdst)
30
30
31 # public functions
31 # public functions
32
32
33 def extract(ui, fileobj):
33 def extract(ui, fileobj):
34 '''extract patch from data read from fileobj.
34 '''extract patch from data read from fileobj.
35
35
36 patch can be normal patch or contained in email message.
36 patch can be normal patch or contained in email message.
37
37
38 return tuple (filename, message, user, date). any item in returned
38 return tuple (filename, message, user, date). any item in returned
39 tuple can be None. if filename is None, fileobj did not contain
39 tuple can be None. if filename is None, fileobj did not contain
40 patch. caller must unlink filename when done.'''
40 patch. caller must unlink filename when done.'''
41
41
42 # attempt to detect the start of a patch
42 # attempt to detect the start of a patch
43 # (this heuristic is borrowed from quilt)
43 # (this heuristic is borrowed from quilt)
44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 '(---|\*\*\*)[ \t])', re.MULTILINE)
46 '(---|\*\*\*)[ \t])', re.MULTILINE)
47
47
48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 tmpfp = os.fdopen(fd, 'w')
49 tmpfp = os.fdopen(fd, 'w')
50 try:
50 try:
51 hgpatch = False
51 hgpatch = False
52
52
53 msg = email.Parser.Parser().parse(fileobj)
53 msg = email.Parser.Parser().parse(fileobj)
54
54
55 message = msg['Subject']
55 message = msg['Subject']
56 user = msg['From']
56 user = msg['From']
57 # should try to parse msg['Date']
57 # should try to parse msg['Date']
58 date = None
58 date = None
59
59
60 if message:
60 if message:
61 message = message.replace('\n\t', ' ')
61 message = message.replace('\n\t', ' ')
62 ui.debug('Subject: %s\n' % message)
62 ui.debug('Subject: %s\n' % message)
63 if user:
63 if user:
64 ui.debug('From: %s\n' % user)
64 ui.debug('From: %s\n' % user)
65 diffs_seen = 0
65 diffs_seen = 0
66 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
66 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
67
67
68 for part in msg.walk():
68 for part in msg.walk():
69 content_type = part.get_content_type()
69 content_type = part.get_content_type()
70 ui.debug('Content-Type: %s\n' % content_type)
70 ui.debug('Content-Type: %s\n' % content_type)
71 if content_type not in ok_types:
71 if content_type not in ok_types:
72 continue
72 continue
73 payload = part.get_payload(decode=True)
73 payload = part.get_payload(decode=True)
74 m = diffre.search(payload)
74 m = diffre.search(payload)
75 if m:
75 if m:
76 ui.debug(_('found patch at byte %d\n') % m.start(0))
76 ui.debug(_('found patch at byte %d\n') % m.start(0))
77 diffs_seen += 1
77 diffs_seen += 1
78 cfp = cStringIO.StringIO()
78 cfp = cStringIO.StringIO()
79 if message:
79 if message:
80 cfp.write(message)
80 cfp.write(message)
81 cfp.write('\n')
81 cfp.write('\n')
82 for line in payload[:m.start(0)].splitlines():
82 for line in payload[:m.start(0)].splitlines():
83 if line.startswith('# HG changeset patch'):
83 if line.startswith('# HG changeset patch'):
84 ui.debug(_('patch generated by hg export\n'))
84 ui.debug(_('patch generated by hg export\n'))
85 hgpatch = True
85 hgpatch = True
86 # drop earlier commit message content
86 # drop earlier commit message content
87 cfp.seek(0)
87 cfp.seek(0)
88 cfp.truncate()
88 cfp.truncate()
89 elif hgpatch:
89 elif hgpatch:
90 if line.startswith('# User '):
90 if line.startswith('# User '):
91 user = line[7:]
91 user = line[7:]
92 ui.debug('From: %s\n' % user)
92 ui.debug('From: %s\n' % user)
93 elif line.startswith("# Date "):
93 elif line.startswith("# Date "):
94 date = line[7:]
94 date = line[7:]
95 if not line.startswith('# '):
95 if not line.startswith('# '):
96 cfp.write(line)
96 cfp.write(line)
97 cfp.write('\n')
97 cfp.write('\n')
98 message = cfp.getvalue()
98 message = cfp.getvalue()
99 if tmpfp:
99 if tmpfp:
100 tmpfp.write(payload)
100 tmpfp.write(payload)
101 if not payload.endswith('\n'):
101 if not payload.endswith('\n'):
102 tmpfp.write('\n')
102 tmpfp.write('\n')
103 elif not diffs_seen and message and content_type == 'text/plain':
103 elif not diffs_seen and message and content_type == 'text/plain':
104 message += '\n' + payload
104 message += '\n' + payload
105 except:
105 except:
106 tmpfp.close()
106 tmpfp.close()
107 os.unlink(tmpname)
107 os.unlink(tmpname)
108 raise
108 raise
109
109
110 tmpfp.close()
110 tmpfp.close()
111 if not diffs_seen:
111 if not diffs_seen:
112 os.unlink(tmpname)
112 os.unlink(tmpname)
113 return None, message, user, date
113 return None, message, user, date
114 return tmpname, message, user, date
114 return tmpname, message, user, date
115
115
116 GP_PATCH = 1 << 0 # we have to run patch
116 GP_PATCH = 1 << 0 # we have to run patch
117 GP_FILTER = 1 << 1 # there's some copy/rename operation
117 GP_FILTER = 1 << 1 # there's some copy/rename operation
118 GP_BINARY = 1 << 2 # there's a binary patch
118 GP_BINARY = 1 << 2 # there's a binary patch
119
119
120 def readgitpatch(patchname):
120 def readgitpatch(patchname):
121 """extract git-style metadata about patches from <patchname>"""
121 """extract git-style metadata about patches from <patchname>"""
122 class gitpatch:
122 class gitpatch:
123 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
123 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
124 def __init__(self, path):
124 def __init__(self, path):
125 self.path = path
125 self.path = path
126 self.oldpath = None
126 self.oldpath = None
127 self.mode = None
127 self.mode = None
128 self.op = 'MODIFY'
128 self.op = 'MODIFY'
129 self.copymod = False
129 self.copymod = False
130 self.lineno = 0
130 self.lineno = 0
131 self.binary = False
131 self.binary = False
132
132
133 # Filter patch for git information
133 # Filter patch for git information
134 gitre = re.compile('diff --git a/(.*) b/(.*)')
134 gitre = re.compile('diff --git a/(.*) b/(.*)')
135 pf = file(patchname)
135 pf = file(patchname)
136 gp = None
136 gp = None
137 gitpatches = []
137 gitpatches = []
138 # Can have a git patch with only metadata, causing patch to complain
138 # Can have a git patch with only metadata, causing patch to complain
139 dopatch = 0
139 dopatch = 0
140
140
141 lineno = 0
141 lineno = 0
142 for line in pf:
142 for line in pf:
143 lineno += 1
143 lineno += 1
144 if line.startswith('diff --git'):
144 if line.startswith('diff --git'):
145 m = gitre.match(line)
145 m = gitre.match(line)
146 if m:
146 if m:
147 if gp:
147 if gp:
148 gitpatches.append(gp)
148 gitpatches.append(gp)
149 src, dst = m.group(1, 2)
149 src, dst = m.group(1, 2)
150 gp = gitpatch(dst)
150 gp = gitpatch(dst)
151 gp.lineno = lineno
151 gp.lineno = lineno
152 elif gp:
152 elif gp:
153 if line.startswith('--- '):
153 if line.startswith('--- '):
154 if gp.op in ('COPY', 'RENAME'):
154 if gp.op in ('COPY', 'RENAME'):
155 gp.copymod = True
155 gp.copymod = True
156 dopatch |= GP_FILTER
156 dopatch |= GP_FILTER
157 gitpatches.append(gp)
157 gitpatches.append(gp)
158 gp = None
158 gp = None
159 dopatch |= GP_PATCH
159 dopatch |= GP_PATCH
160 continue
160 continue
161 if line.startswith('rename from '):
161 if line.startswith('rename from '):
162 gp.op = 'RENAME'
162 gp.op = 'RENAME'
163 gp.oldpath = line[12:].rstrip()
163 gp.oldpath = line[12:].rstrip()
164 elif line.startswith('rename to '):
164 elif line.startswith('rename to '):
165 gp.path = line[10:].rstrip()
165 gp.path = line[10:].rstrip()
166 elif line.startswith('copy from '):
166 elif line.startswith('copy from '):
167 gp.op = 'COPY'
167 gp.op = 'COPY'
168 gp.oldpath = line[10:].rstrip()
168 gp.oldpath = line[10:].rstrip()
169 elif line.startswith('copy to '):
169 elif line.startswith('copy to '):
170 gp.path = line[8:].rstrip()
170 gp.path = line[8:].rstrip()
171 elif line.startswith('deleted file'):
171 elif line.startswith('deleted file'):
172 gp.op = 'DELETE'
172 gp.op = 'DELETE'
173 elif line.startswith('new file mode '):
173 elif line.startswith('new file mode '):
174 gp.op = 'ADD'
174 gp.op = 'ADD'
175 gp.mode = int(line.rstrip()[-3:], 8)
175 gp.mode = int(line.rstrip()[-3:], 8)
176 elif line.startswith('new mode '):
176 elif line.startswith('new mode '):
177 gp.mode = int(line.rstrip()[-3:], 8)
177 gp.mode = int(line.rstrip()[-3:], 8)
178 elif line.startswith('GIT binary patch'):
178 elif line.startswith('GIT binary patch'):
179 dopatch |= GP_BINARY
179 dopatch |= GP_BINARY
180 gp.binary = True
180 gp.binary = True
181 if gp:
181 if gp:
182 gitpatches.append(gp)
182 gitpatches.append(gp)
183
183
184 if not gitpatches:
184 if not gitpatches:
185 dopatch = GP_PATCH
185 dopatch = GP_PATCH
186
186
187 return (dopatch, gitpatches)
187 return (dopatch, gitpatches)
188
188
189 def dogitpatch(patchname, gitpatches, cwd=None):
189 def dogitpatch(patchname, gitpatches, cwd=None):
190 """Preprocess git patch so that vanilla patch can handle it"""
190 """Preprocess git patch so that vanilla patch can handle it"""
191 def extractbin(fp):
191 def extractbin(fp):
192 i = [0] # yuck
192 i = [0] # yuck
193 def readline():
193 def readline():
194 i[0] += 1
194 i[0] += 1
195 return fp.readline().rstrip()
195 return fp.readline().rstrip()
196 line = readline()
196 line = readline()
197 while line and not line.startswith('literal '):
197 while line and not line.startswith('literal '):
198 line = readline()
198 line = readline()
199 if not line:
199 if not line:
200 return None, i[0]
200 return None, i[0]
201 size = int(line[8:])
201 size = int(line[8:])
202 dec = []
202 dec = []
203 line = readline()
203 line = readline()
204 while line:
204 while line:
205 l = line[0]
205 l = line[0]
206 if l <= 'Z' and l >= 'A':
206 if l <= 'Z' and l >= 'A':
207 l = ord(l) - ord('A') + 1
207 l = ord(l) - ord('A') + 1
208 else:
208 else:
209 l = ord(l) - ord('a') + 27
209 l = ord(l) - ord('a') + 27
210 dec.append(base85.b85decode(line[1:])[:l])
210 dec.append(base85.b85decode(line[1:])[:l])
211 line = readline()
211 line = readline()
212 text = zlib.decompress(''.join(dec))
212 text = zlib.decompress(''.join(dec))
213 if len(text) != size:
213 if len(text) != size:
214 raise util.Abort(_('binary patch is %d bytes, not %d') %
214 raise util.Abort(_('binary patch is %d bytes, not %d') %
215 (len(text), size))
215 (len(text), size))
216 return text, i[0]
216 return text, i[0]
217
217
218 pf = file(patchname)
218 pf = file(patchname)
219 pfline = 1
219 pfline = 1
220
220
221 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
221 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
222 tmpfp = os.fdopen(fd, 'w')
222 tmpfp = os.fdopen(fd, 'w')
223
223
224 try:
224 try:
225 for i in xrange(len(gitpatches)):
225 for i in xrange(len(gitpatches)):
226 p = gitpatches[i]
226 p = gitpatches[i]
227 if not p.copymod and not p.binary:
227 if not p.copymod and not p.binary:
228 continue
228 continue
229
229
230 # rewrite patch hunk
230 # rewrite patch hunk
231 while pfline < p.lineno:
231 while pfline < p.lineno:
232 tmpfp.write(pf.readline())
232 tmpfp.write(pf.readline())
233 pfline += 1
233 pfline += 1
234
234
235 if p.binary:
235 if p.binary:
236 text, delta = extractbin(pf)
236 text, delta = extractbin(pf)
237 if not text:
237 if not text:
238 raise util.Abort(_('binary patch extraction failed'))
238 raise util.Abort(_('binary patch extraction failed'))
239 pfline += delta
239 pfline += delta
240 if not cwd:
240 if not cwd:
241 cwd = os.getcwd()
241 cwd = os.getcwd()
242 absdst = os.path.join(cwd, p.path)
242 absdst = os.path.join(cwd, p.path)
243 basedir = os.path.dirname(absdst)
243 basedir = os.path.dirname(absdst)
244 if not os.path.isdir(basedir):
244 if not os.path.isdir(basedir):
245 os.makedirs(basedir)
245 os.makedirs(basedir)
246 out = file(absdst, 'wb')
246 out = file(absdst, 'wb')
247 out.write(text)
247 out.write(text)
248 out.close()
248 out.close()
249 elif p.copymod:
249 elif p.copymod:
250 copyfile(p.oldpath, p.path, basedir=cwd)
250 copyfile(p.oldpath, p.path, basedir=cwd)
251 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
251 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
252 line = pf.readline()
252 line = pf.readline()
253 pfline += 1
253 pfline += 1
254 while not line.startswith('--- a/'):
254 while not line.startswith('--- a/'):
255 tmpfp.write(line)
255 tmpfp.write(line)
256 line = pf.readline()
256 line = pf.readline()
257 pfline += 1
257 pfline += 1
258 tmpfp.write('--- a/%s\n' % p.path)
258 tmpfp.write('--- a/%s\n' % p.path)
259
259
260 line = pf.readline()
260 line = pf.readline()
261 while line:
261 while line:
262 tmpfp.write(line)
262 tmpfp.write(line)
263 line = pf.readline()
263 line = pf.readline()
264 except:
264 except:
265 tmpfp.close()
265 tmpfp.close()
266 os.unlink(patchname)
266 os.unlink(patchname)
267 raise
267 raise
268
268
269 tmpfp.close()
269 tmpfp.close()
270 return patchname
270 return patchname
271
271
272 def patch(patchname, ui, strip=1, cwd=None, files={}):
272 def patch(patchname, ui, strip=1, cwd=None, files={}):
273 """apply the patch <patchname> to the working directory.
273 """apply the patch <patchname> to the working directory.
274 a list of patched files is returned"""
274 a list of patched files is returned"""
275
275
276 # helper function
276 # helper function
277 def __patch(patchname):
277 def __patch(patchname):
278 """patch and updates the files and fuzz variables"""
278 """patch and updates the files and fuzz variables"""
279 fuzz = False
279 fuzz = False
280
280
281 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
281 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
282 'patch')
282 'patch')
283 args = []
283 args = []
284 if cwd:
284 if cwd:
285 args.append('-d %s' % util.shellquote(cwd))
285 args.append('-d %s' % util.shellquote(cwd))
286 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
286 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
287 util.shellquote(patchname)))
287 util.shellquote(patchname)))
288
288
289 for line in fp:
289 for line in fp:
290 line = line.rstrip()
290 line = line.rstrip()
291 ui.note(line + '\n')
291 ui.note(line + '\n')
292 if line.startswith('patching file '):
292 if line.startswith('patching file '):
293 pf = util.parse_patch_output(line)
293 pf = util.parse_patch_output(line)
294 printed_file = False
294 printed_file = False
295 files.setdefault(pf, (None, None))
295 files.setdefault(pf, (None, None))
296 elif line.find('with fuzz') >= 0:
296 elif line.find('with fuzz') >= 0:
297 fuzz = True
297 fuzz = True
298 if not printed_file:
298 if not printed_file:
299 ui.warn(pf + '\n')
299 ui.warn(pf + '\n')
300 printed_file = True
300 printed_file = True
301 ui.warn(line + '\n')
301 ui.warn(line + '\n')
302 elif line.find('saving rejects to file') >= 0:
302 elif line.find('saving rejects to file') >= 0:
303 ui.warn(line + '\n')
303 ui.warn(line + '\n')
304 elif line.find('FAILED') >= 0:
304 elif line.find('FAILED') >= 0:
305 if not printed_file:
305 if not printed_file:
306 ui.warn(pf + '\n')
306 ui.warn(pf + '\n')
307 printed_file = True
307 printed_file = True
308 ui.warn(line + '\n')
308 ui.warn(line + '\n')
309 code = fp.close()
309 code = fp.close()
310 if code:
310 if code:
311 raise util.Abort(_("patch command failed: %s") %
311 raise util.Abort(_("patch command failed: %s") %
312 util.explain_exit(code)[0])
312 util.explain_exit(code)[0])
313 return fuzz
313 return fuzz
314
314
315 (dopatch, gitpatches) = readgitpatch(patchname)
315 (dopatch, gitpatches) = readgitpatch(patchname)
316 for gp in gitpatches:
316 for gp in gitpatches:
317 files[gp.path] = (gp.op, gp)
317 files[gp.path] = (gp.op, gp)
318
318
319 fuzz = False
319 fuzz = False
320 if dopatch:
320 if dopatch:
321 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
321 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
322 if filterpatch:
322 if filterpatch:
323 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
323 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
324 try:
324 try:
325 if dopatch & GP_PATCH:
325 if dopatch & GP_PATCH:
326 fuzz = __patch(patchname)
326 fuzz = __patch(patchname)
327 finally:
327 finally:
328 if filterpatch:
328 if filterpatch:
329 os.unlink(patchname)
329 os.unlink(patchname)
330
330
331 return fuzz
331 return fuzz
332
332
333 def diffopts(ui, opts={}, untrusted=False):
333 def diffopts(ui, opts={}, untrusted=False):
334 def get(key, name=None):
334 def get(key, name=None):
335 return (opts.get(key) or
335 return (opts.get(key) or
336 ui.configbool('diff', name or key, None, untrusted=untrusted))
336 ui.configbool('diff', name or key, None, untrusted=untrusted))
337 return mdiff.diffopts(
337 return mdiff.diffopts(
338 text=opts.get('text'),
338 text=opts.get('text'),
339 git=get('git'),
339 git=get('git'),
340 nodates=get('nodates'),
340 nodates=get('nodates'),
341 showfunc=get('show_function', 'showfunc'),
341 showfunc=get('show_function', 'showfunc'),
342 ignorews=get('ignore_all_space', 'ignorews'),
342 ignorews=get('ignore_all_space', 'ignorews'),
343 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
343 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
344 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
344 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
345
345
346 def updatedir(ui, repo, patches, wlock=None):
346 def updatedir(ui, repo, patches, wlock=None):
347 '''Update dirstate after patch application according to metadata'''
347 '''Update dirstate after patch application according to metadata'''
348 if not patches:
348 if not patches:
349 return
349 return
350 copies = []
350 copies = []
351 removes = {}
351 removes = {}
352 cfiles = patches.keys()
352 cfiles = patches.keys()
353 cwd = repo.getcwd()
353 cwd = repo.getcwd()
354 if cwd:
354 if cwd:
355 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
355 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
356 for f in patches:
356 for f in patches:
357 ctype, gp = patches[f]
357 ctype, gp = patches[f]
358 if ctype == 'RENAME':
358 if ctype == 'RENAME':
359 copies.append((gp.oldpath, gp.path, gp.copymod))
359 copies.append((gp.oldpath, gp.path, gp.copymod))
360 removes[gp.oldpath] = 1
360 removes[gp.oldpath] = 1
361 elif ctype == 'COPY':
361 elif ctype == 'COPY':
362 copies.append((gp.oldpath, gp.path, gp.copymod))
362 copies.append((gp.oldpath, gp.path, gp.copymod))
363 elif ctype == 'DELETE':
363 elif ctype == 'DELETE':
364 removes[gp.path] = 1
364 removes[gp.path] = 1
365 for src, dst, after in copies:
365 for src, dst, after in copies:
366 if not after:
366 if not after:
367 copyfile(src, dst, repo.root)
367 copyfile(src, dst, repo.root)
368 repo.copy(src, dst, wlock=wlock)
368 repo.copy(src, dst, wlock=wlock)
369 removes = removes.keys()
369 removes = removes.keys()
370 if removes:
370 if removes:
371 removes.sort()
371 removes.sort()
372 repo.remove(removes, True, wlock=wlock)
372 repo.remove(removes, True, wlock=wlock)
373 for f in patches:
373 for f in patches:
374 ctype, gp = patches[f]
374 ctype, gp = patches[f]
375 if gp and gp.mode:
375 if gp and gp.mode:
376 x = gp.mode & 0100 != 0
376 x = gp.mode & 0100 != 0
377 dst = os.path.join(repo.root, gp.path)
377 dst = os.path.join(repo.root, gp.path)
378 # patch won't create empty files
378 # patch won't create empty files
379 if ctype == 'ADD' and not os.path.exists(dst):
379 if ctype == 'ADD' and not os.path.exists(dst):
380 repo.wwrite(gp.path, '', x and 'x' or '')
380 repo.wwrite(gp.path, '', x and 'x' or '')
381 else:
381 else:
382 util.set_exec(dst, x)
382 util.set_exec(dst, x)
383 cmdutil.addremove(repo, cfiles, wlock=wlock)
383 cmdutil.addremove(repo, cfiles, wlock=wlock)
384 files = patches.keys()
384 files = patches.keys()
385 files.extend([r for r in removes if r not in files])
385 files.extend([r for r in removes if r not in files])
386 files.sort()
386 files.sort()
387
387
388 return files
388 return files
389
389
390 def b85diff(fp, to, tn):
390 def b85diff(fp, to, tn):
391 '''print base85-encoded binary diff'''
391 '''print base85-encoded binary diff'''
392 def gitindex(text):
392 def gitindex(text):
393 if not text:
393 if not text:
394 return '0' * 40
394 return '0' * 40
395 l = len(text)
395 l = len(text)
396 s = sha.new('blob %d\0' % l)
396 s = sha.new('blob %d\0' % l)
397 s.update(text)
397 s.update(text)
398 return s.hexdigest()
398 return s.hexdigest()
399
399
400 def fmtline(line):
400 def fmtline(line):
401 l = len(line)
401 l = len(line)
402 if l <= 26:
402 if l <= 26:
403 l = chr(ord('A') + l - 1)
403 l = chr(ord('A') + l - 1)
404 else:
404 else:
405 l = chr(l - 26 + ord('a') - 1)
405 l = chr(l - 26 + ord('a') - 1)
406 return '%c%s\n' % (l, base85.b85encode(line, True))
406 return '%c%s\n' % (l, base85.b85encode(line, True))
407
407
408 def chunk(text, csize=52):
408 def chunk(text, csize=52):
409 l = len(text)
409 l = len(text)
410 i = 0
410 i = 0
411 while i < l:
411 while i < l:
412 yield text[i:i+csize]
412 yield text[i:i+csize]
413 i += csize
413 i += csize
414
414
415 if to == tn:
416 return
415 # TODO: deltas
417 # TODO: deltas
416 l = len(tn)
418 l = len(tn)
417 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
419 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
418 (gitindex(to), gitindex(tn), len(tn)))
420 (gitindex(to), gitindex(tn), len(tn)))
419
421
420 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
422 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
421 fp.write(tn)
423 fp.write(tn)
422 fp.write('\n')
424 fp.write('\n')
423
425
424 def diff(repo, node1=None, node2=None, files=None, match=util.always,
426 def diff(repo, node1=None, node2=None, files=None, match=util.always,
425 fp=None, changes=None, opts=None):
427 fp=None, changes=None, opts=None):
426 '''print diff of changes to files between two nodes, or node and
428 '''print diff of changes to files between two nodes, or node and
427 working directory.
429 working directory.
428
430
429 if node1 is None, use first dirstate parent instead.
431 if node1 is None, use first dirstate parent instead.
430 if node2 is None, compare node1 with working directory.'''
432 if node2 is None, compare node1 with working directory.'''
431
433
432 if opts is None:
434 if opts is None:
433 opts = mdiff.defaultopts
435 opts = mdiff.defaultopts
434 if fp is None:
436 if fp is None:
435 fp = repo.ui
437 fp = repo.ui
436
438
437 if not node1:
439 if not node1:
438 node1 = repo.dirstate.parents()[0]
440 node1 = repo.dirstate.parents()[0]
439
441
440 ccache = {}
442 ccache = {}
441 def getctx(r):
443 def getctx(r):
442 if r not in ccache:
444 if r not in ccache:
443 ccache[r] = context.changectx(repo, r)
445 ccache[r] = context.changectx(repo, r)
444 return ccache[r]
446 return ccache[r]
445
447
446 flcache = {}
448 flcache = {}
447 def getfilectx(f, ctx):
449 def getfilectx(f, ctx):
448 flctx = ctx.filectx(f, filelog=flcache.get(f))
450 flctx = ctx.filectx(f, filelog=flcache.get(f))
449 if f not in flcache:
451 if f not in flcache:
450 flcache[f] = flctx._filelog
452 flcache[f] = flctx._filelog
451 return flctx
453 return flctx
452
454
453 # reading the data for node1 early allows it to play nicely
455 # reading the data for node1 early allows it to play nicely
454 # with repo.status and the revlog cache.
456 # with repo.status and the revlog cache.
455 ctx1 = context.changectx(repo, node1)
457 ctx1 = context.changectx(repo, node1)
456 # force manifest reading
458 # force manifest reading
457 man1 = ctx1.manifest()
459 man1 = ctx1.manifest()
458 date1 = util.datestr(ctx1.date())
460 date1 = util.datestr(ctx1.date())
459
461
460 if not changes:
462 if not changes:
461 changes = repo.status(node1, node2, files, match=match)[:5]
463 changes = repo.status(node1, node2, files, match=match)[:5]
462 modified, added, removed, deleted, unknown = changes
464 modified, added, removed, deleted, unknown = changes
463 if files:
465 if files:
464 def filterfiles(filters):
466 def filterfiles(filters):
465 l = [x for x in filters if x in files]
467 l = [x for x in filters if x in files]
466
468
467 for t in files:
469 for t in files:
468 if not t.endswith("/"):
470 if not t.endswith("/"):
469 t += "/"
471 t += "/"
470 l += [x for x in filters if x.startswith(t)]
472 l += [x for x in filters if x.startswith(t)]
471 return l
473 return l
472
474
473 modified, added, removed = map(filterfiles, (modified, added, removed))
475 modified, added, removed = map(filterfiles, (modified, added, removed))
474
476
475 if not modified and not added and not removed:
477 if not modified and not added and not removed:
476 return
478 return
477
479
478 if node2:
480 if node2:
479 ctx2 = context.changectx(repo, node2)
481 ctx2 = context.changectx(repo, node2)
480 else:
482 else:
481 ctx2 = context.workingctx(repo)
483 ctx2 = context.workingctx(repo)
482 man2 = ctx2.manifest()
484 man2 = ctx2.manifest()
483
485
484 # returns False if there was no rename between ctx1 and ctx2
486 # returns False if there was no rename between ctx1 and ctx2
485 # returns None if the file was created between ctx1 and ctx2
487 # returns None if the file was created between ctx1 and ctx2
486 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
488 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
487 def renamed(f):
489 def renamed(f):
488 startrev = ctx1.rev()
490 startrev = ctx1.rev()
489 c = ctx2
491 c = ctx2
490 crev = c.rev()
492 crev = c.rev()
491 if crev is None:
493 if crev is None:
492 crev = repo.changelog.count()
494 crev = repo.changelog.count()
493 orig = f
495 orig = f
494 while crev > startrev:
496 while crev > startrev:
495 if f in c.files():
497 if f in c.files():
496 try:
498 try:
497 src = getfilectx(f, c).renamed()
499 src = getfilectx(f, c).renamed()
498 except revlog.LookupError:
500 except revlog.LookupError:
499 return None
501 return None
500 if src:
502 if src:
501 f = src[0]
503 f = src[0]
502 crev = c.parents()[0].rev()
504 crev = c.parents()[0].rev()
503 # try to reuse
505 # try to reuse
504 c = getctx(crev)
506 c = getctx(crev)
505 if f not in man1:
507 if f not in man1:
506 return None
508 return None
507 if f == orig:
509 if f == orig:
508 return False
510 return False
509 return f
511 return f
510
512
511 if repo.ui.quiet:
513 if repo.ui.quiet:
512 r = None
514 r = None
513 else:
515 else:
514 hexfunc = repo.ui.debugflag and hex or short
516 hexfunc = repo.ui.debugflag and hex or short
515 r = [hexfunc(node) for node in [node1, node2] if node]
517 r = [hexfunc(node) for node in [node1, node2] if node]
516
518
517 if opts.git:
519 if opts.git:
518 copied = {}
520 copied = {}
519 for f in added:
521 for f in added:
520 src = renamed(f)
522 src = renamed(f)
521 if src:
523 if src:
522 copied[f] = src
524 copied[f] = src
523 srcs = [x[1] for x in copied.items()]
525 srcs = [x[1] for x in copied.items()]
524
526
525 all = modified + added + removed
527 all = modified + added + removed
526 all.sort()
528 all.sort()
527 gone = {}
529 gone = {}
528
530
529 for f in all:
531 for f in all:
530 to = None
532 to = None
531 tn = None
533 tn = None
532 dodiff = True
534 dodiff = True
533 header = []
535 header = []
534 if f in man1:
536 if f in man1:
535 to = getfilectx(f, ctx1).data()
537 to = getfilectx(f, ctx1).data()
536 if f not in removed:
538 if f not in removed:
537 tn = getfilectx(f, ctx2).data()
539 tn = getfilectx(f, ctx2).data()
538 if opts.git:
540 if opts.git:
539 def gitmode(x):
541 def gitmode(x):
540 return x and '100755' or '100644'
542 return x and '100755' or '100644'
541 def addmodehdr(header, omode, nmode):
543 def addmodehdr(header, omode, nmode):
542 if omode != nmode:
544 if omode != nmode:
543 header.append('old mode %s\n' % omode)
545 header.append('old mode %s\n' % omode)
544 header.append('new mode %s\n' % nmode)
546 header.append('new mode %s\n' % nmode)
545
547
546 a, b = f, f
548 a, b = f, f
547 if f in added:
549 if f in added:
548 mode = gitmode(man2.execf(f))
550 mode = gitmode(man2.execf(f))
549 if f in copied:
551 if f in copied:
550 a = copied[f]
552 a = copied[f]
551 omode = gitmode(man1.execf(a))
553 omode = gitmode(man1.execf(a))
552 addmodehdr(header, omode, mode)
554 addmodehdr(header, omode, mode)
553 if a in removed and a not in gone:
555 if a in removed and a not in gone:
554 op = 'rename'
556 op = 'rename'
555 gone[a] = 1
557 gone[a] = 1
556 else:
558 else:
557 op = 'copy'
559 op = 'copy'
558 header.append('%s from %s\n' % (op, a))
560 header.append('%s from %s\n' % (op, a))
559 header.append('%s to %s\n' % (op, f))
561 header.append('%s to %s\n' % (op, f))
560 to = getfilectx(a, ctx1).data()
562 to = getfilectx(a, ctx1).data()
561 else:
563 else:
562 header.append('new file mode %s\n' % mode)
564 header.append('new file mode %s\n' % mode)
563 if util.binary(tn):
565 if util.binary(tn):
564 dodiff = 'binary'
566 dodiff = 'binary'
565 elif f in removed:
567 elif f in removed:
566 if f in srcs:
568 if f in srcs:
567 dodiff = False
569 dodiff = False
568 else:
570 else:
569 mode = gitmode(man1.execf(f))
571 mode = gitmode(man1.execf(f))
570 header.append('deleted file mode %s\n' % mode)
572 header.append('deleted file mode %s\n' % mode)
571 else:
573 else:
572 omode = gitmode(man1.execf(f))
574 omode = gitmode(man1.execf(f))
573 nmode = gitmode(man2.execf(f))
575 nmode = gitmode(man2.execf(f))
574 addmodehdr(header, omode, nmode)
576 addmodehdr(header, omode, nmode)
575 if util.binary(to) or util.binary(tn):
577 if util.binary(to) or util.binary(tn):
576 dodiff = 'binary'
578 dodiff = 'binary'
577 r = None
579 r = None
578 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
580 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
579 if dodiff == 'binary':
581 if dodiff == 'binary':
580 fp.write(''.join(header))
582 fp.write(''.join(header))
581 b85diff(fp, to, tn)
583 b85diff(fp, to, tn)
582 elif dodiff:
584 elif dodiff:
583 text = mdiff.unidiff(to, date1,
585 text = mdiff.unidiff(to, date1,
584 # ctx2 date may be dynamic
586 # ctx2 date may be dynamic
585 tn, util.datestr(ctx2.date()),
587 tn, util.datestr(ctx2.date()),
586 f, r, opts=opts)
588 f, r, opts=opts)
587 if text or len(header) > 1:
589 if text or len(header) > 1:
588 fp.write(''.join(header))
590 fp.write(''.join(header))
589 fp.write(text)
591 fp.write(text)
590
592
591 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
593 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
592 opts=None):
594 opts=None):
593 '''export changesets as hg patches.'''
595 '''export changesets as hg patches.'''
594
596
595 total = len(revs)
597 total = len(revs)
596 revwidth = max([len(str(rev)) for rev in revs])
598 revwidth = max([len(str(rev)) for rev in revs])
597
599
598 def single(rev, seqno, fp):
600 def single(rev, seqno, fp):
599 ctx = repo.changectx(rev)
601 ctx = repo.changectx(rev)
600 node = ctx.node()
602 node = ctx.node()
601 parents = [p.node() for p in ctx.parents() if p]
603 parents = [p.node() for p in ctx.parents() if p]
602 if switch_parent:
604 if switch_parent:
603 parents.reverse()
605 parents.reverse()
604 prev = (parents and parents[0]) or nullid
606 prev = (parents and parents[0]) or nullid
605
607
606 if not fp:
608 if not fp:
607 fp = cmdutil.make_file(repo, template, node, total=total,
609 fp = cmdutil.make_file(repo, template, node, total=total,
608 seqno=seqno, revwidth=revwidth)
610 seqno=seqno, revwidth=revwidth)
609 if fp not in (sys.stdout, repo.ui):
611 if fp not in (sys.stdout, repo.ui):
610 repo.ui.note("%s\n" % fp.name)
612 repo.ui.note("%s\n" % fp.name)
611
613
612 fp.write("# HG changeset patch\n")
614 fp.write("# HG changeset patch\n")
613 fp.write("# User %s\n" % ctx.user())
615 fp.write("# User %s\n" % ctx.user())
614 fp.write("# Date %d %d\n" % ctx.date())
616 fp.write("# Date %d %d\n" % ctx.date())
615 fp.write("# Node ID %s\n" % hex(node))
617 fp.write("# Node ID %s\n" % hex(node))
616 fp.write("# Parent %s\n" % hex(prev))
618 fp.write("# Parent %s\n" % hex(prev))
617 if len(parents) > 1:
619 if len(parents) > 1:
618 fp.write("# Parent %s\n" % hex(parents[1]))
620 fp.write("# Parent %s\n" % hex(parents[1]))
619 fp.write(ctx.description().rstrip())
621 fp.write(ctx.description().rstrip())
620 fp.write("\n\n")
622 fp.write("\n\n")
621
623
622 diff(repo, prev, node, fp=fp, opts=opts)
624 diff(repo, prev, node, fp=fp, opts=opts)
623 if fp not in (sys.stdout, repo.ui):
625 if fp not in (sys.stdout, repo.ui):
624 fp.close()
626 fp.close()
625
627
626 for seqno, rev in enumerate(revs):
628 for seqno, rev in enumerate(revs):
627 single(rev, seqno+1, fp)
629 single(rev, seqno+1, fp)
628
630
629 def diffstat(patchlines):
631 def diffstat(patchlines):
630 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
632 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
631 try:
633 try:
632 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
634 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
633 try:
635 try:
634 for line in patchlines: print >> p.tochild, line
636 for line in patchlines: print >> p.tochild, line
635 p.tochild.close()
637 p.tochild.close()
636 if p.wait(): return
638 if p.wait(): return
637 fp = os.fdopen(fd, 'r')
639 fp = os.fdopen(fd, 'r')
638 stat = []
640 stat = []
639 for line in fp: stat.append(line.lstrip())
641 for line in fp: stat.append(line.lstrip())
640 last = stat.pop()
642 last = stat.pop()
641 stat.insert(0, last)
643 stat.insert(0, last)
642 stat = ''.join(stat)
644 stat = ''.join(stat)
643 if stat.startswith('0 files'): raise ValueError
645 if stat.startswith('0 files'): raise ValueError
644 return stat
646 return stat
645 except: raise
647 except: raise
646 finally:
648 finally:
647 try: os.unlink(name)
649 try: os.unlink(name)
648 except: pass
650 except: pass
@@ -1,1425 +1,1443 b''
1 """
1 """
2 util.py - Mercurial utility functions and platform specfic implementations
2 util.py - Mercurial utility functions and platform specfic implementations
3
3
4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7
7
8 This software may be used and distributed according to the terms
8 This software may be used and distributed according to the terms
9 of the GNU General Public License, incorporated herein by reference.
9 of the GNU General Public License, incorporated herein by reference.
10
10
11 This contains helper routines that are independent of the SCM core and hide
11 This contains helper routines that are independent of the SCM core and hide
12 platform-specific details from the core.
12 platform-specific details from the core.
13 """
13 """
14
14
15 from i18n import _
15 from i18n import _
16 import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile
16 import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile
17 import os, threading, time, calendar, ConfigParser, locale, glob
17 import os, threading, time, calendar, ConfigParser, locale, glob
18
18
19 try:
19 try:
20 _encoding = os.environ.get("HGENCODING") or locale.getpreferredencoding() \
20 _encoding = os.environ.get("HGENCODING") or locale.getpreferredencoding() \
21 or "ascii"
21 or "ascii"
22 except locale.Error:
22 except locale.Error:
23 _encoding = 'ascii'
23 _encoding = 'ascii'
24 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
24 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
25 _fallbackencoding = 'ISO-8859-1'
25 _fallbackencoding = 'ISO-8859-1'
26
26
27 def tolocal(s):
27 def tolocal(s):
28 """
28 """
29 Convert a string from internal UTF-8 to local encoding
29 Convert a string from internal UTF-8 to local encoding
30
30
31 All internal strings should be UTF-8 but some repos before the
31 All internal strings should be UTF-8 but some repos before the
32 implementation of locale support may contain latin1 or possibly
32 implementation of locale support may contain latin1 or possibly
33 other character sets. We attempt to decode everything strictly
33 other character sets. We attempt to decode everything strictly
34 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
34 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
35 replace unknown characters.
35 replace unknown characters.
36 """
36 """
37 for e in ('UTF-8', _fallbackencoding):
37 for e in ('UTF-8', _fallbackencoding):
38 try:
38 try:
39 u = s.decode(e) # attempt strict decoding
39 u = s.decode(e) # attempt strict decoding
40 return u.encode(_encoding, "replace")
40 return u.encode(_encoding, "replace")
41 except LookupError, k:
41 except LookupError, k:
42 raise Abort(_("%s, please check your locale settings") % k)
42 raise Abort(_("%s, please check your locale settings") % k)
43 except UnicodeDecodeError:
43 except UnicodeDecodeError:
44 pass
44 pass
45 u = s.decode("utf-8", "replace") # last ditch
45 u = s.decode("utf-8", "replace") # last ditch
46 return u.encode(_encoding, "replace")
46 return u.encode(_encoding, "replace")
47
47
48 def fromlocal(s):
48 def fromlocal(s):
49 """
49 """
50 Convert a string from the local character encoding to UTF-8
50 Convert a string from the local character encoding to UTF-8
51
51
52 We attempt to decode strings using the encoding mode set by
52 We attempt to decode strings using the encoding mode set by
53 HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown
53 HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown
54 characters will cause an error message. Other modes include
54 characters will cause an error message. Other modes include
55 'replace', which replaces unknown characters with a special
55 'replace', which replaces unknown characters with a special
56 Unicode character, and 'ignore', which drops the character.
56 Unicode character, and 'ignore', which drops the character.
57 """
57 """
58 try:
58 try:
59 return s.decode(_encoding, _encodingmode).encode("utf-8")
59 return s.decode(_encoding, _encodingmode).encode("utf-8")
60 except UnicodeDecodeError, inst:
60 except UnicodeDecodeError, inst:
61 sub = s[max(0, inst.start-10):inst.start+10]
61 sub = s[max(0, inst.start-10):inst.start+10]
62 raise Abort("decoding near '%s': %s!" % (sub, inst))
62 raise Abort("decoding near '%s': %s!" % (sub, inst))
63 except LookupError, k:
63 except LookupError, k:
64 raise Abort(_("%s, please check your locale settings") % k)
64 raise Abort(_("%s, please check your locale settings") % k)
65
65
66 def locallen(s):
66 def locallen(s):
67 """Find the length in characters of a local string"""
67 """Find the length in characters of a local string"""
68 return len(s.decode(_encoding, "replace"))
68 return len(s.decode(_encoding, "replace"))
69
69
70 def localsub(s, a, b=None):
70 def localsub(s, a, b=None):
71 try:
71 try:
72 u = s.decode(_encoding, _encodingmode)
72 u = s.decode(_encoding, _encodingmode)
73 if b is not None:
73 if b is not None:
74 u = u[a:b]
74 u = u[a:b]
75 else:
75 else:
76 u = u[:a]
76 u = u[:a]
77 return u.encode(_encoding, _encodingmode)
77 return u.encode(_encoding, _encodingmode)
78 except UnicodeDecodeError, inst:
78 except UnicodeDecodeError, inst:
79 sub = s[max(0, inst.start-10), inst.start+10]
79 sub = s[max(0, inst.start-10), inst.start+10]
80 raise Abort(_("decoding near '%s': %s!\n") % (sub, inst))
80 raise Abort(_("decoding near '%s': %s!\n") % (sub, inst))
81
81
82 # used by parsedate
82 # used by parsedate
83 defaultdateformats = (
83 defaultdateformats = (
84 '%Y-%m-%d %H:%M:%S',
84 '%Y-%m-%d %H:%M:%S',
85 '%Y-%m-%d %I:%M:%S%p',
85 '%Y-%m-%d %I:%M:%S%p',
86 '%Y-%m-%d %H:%M',
86 '%Y-%m-%d %H:%M',
87 '%Y-%m-%d %I:%M%p',
87 '%Y-%m-%d %I:%M%p',
88 '%Y-%m-%d',
88 '%Y-%m-%d',
89 '%m-%d',
89 '%m-%d',
90 '%m/%d',
90 '%m/%d',
91 '%m/%d/%y',
91 '%m/%d/%y',
92 '%m/%d/%Y',
92 '%m/%d/%Y',
93 '%a %b %d %H:%M:%S %Y',
93 '%a %b %d %H:%M:%S %Y',
94 '%a %b %d %I:%M:%S%p %Y',
94 '%a %b %d %I:%M:%S%p %Y',
95 '%b %d %H:%M:%S %Y',
95 '%b %d %H:%M:%S %Y',
96 '%b %d %I:%M:%S%p %Y',
96 '%b %d %I:%M:%S%p %Y',
97 '%b %d %H:%M:%S',
97 '%b %d %H:%M:%S',
98 '%b %d %I:%M:%S%p',
98 '%b %d %I:%M:%S%p',
99 '%b %d %H:%M',
99 '%b %d %H:%M',
100 '%b %d %I:%M%p',
100 '%b %d %I:%M%p',
101 '%b %d %Y',
101 '%b %d %Y',
102 '%b %d',
102 '%b %d',
103 '%H:%M:%S',
103 '%H:%M:%S',
104 '%I:%M:%SP',
104 '%I:%M:%SP',
105 '%H:%M',
105 '%H:%M',
106 '%I:%M%p',
106 '%I:%M%p',
107 )
107 )
108
108
109 extendeddateformats = defaultdateformats + (
109 extendeddateformats = defaultdateformats + (
110 "%Y",
110 "%Y",
111 "%Y-%m",
111 "%Y-%m",
112 "%b",
112 "%b",
113 "%b %Y",
113 "%b %Y",
114 )
114 )
115
115
116 class SignalInterrupt(Exception):
116 class SignalInterrupt(Exception):
117 """Exception raised on SIGTERM and SIGHUP."""
117 """Exception raised on SIGTERM and SIGHUP."""
118
118
119 # differences from SafeConfigParser:
119 # differences from SafeConfigParser:
120 # - case-sensitive keys
120 # - case-sensitive keys
121 # - allows values that are not strings (this means that you may not
121 # - allows values that are not strings (this means that you may not
122 # be able to save the configuration to a file)
122 # be able to save the configuration to a file)
123 class configparser(ConfigParser.SafeConfigParser):
123 class configparser(ConfigParser.SafeConfigParser):
124 def optionxform(self, optionstr):
124 def optionxform(self, optionstr):
125 return optionstr
125 return optionstr
126
126
127 def set(self, section, option, value):
127 def set(self, section, option, value):
128 return ConfigParser.ConfigParser.set(self, section, option, value)
128 return ConfigParser.ConfigParser.set(self, section, option, value)
129
129
130 def _interpolate(self, section, option, rawval, vars):
130 def _interpolate(self, section, option, rawval, vars):
131 if not isinstance(rawval, basestring):
131 if not isinstance(rawval, basestring):
132 return rawval
132 return rawval
133 return ConfigParser.SafeConfigParser._interpolate(self, section,
133 return ConfigParser.SafeConfigParser._interpolate(self, section,
134 option, rawval, vars)
134 option, rawval, vars)
135
135
136 def cachefunc(func):
136 def cachefunc(func):
137 '''cache the result of function calls'''
137 '''cache the result of function calls'''
138 # XXX doesn't handle keywords args
138 # XXX doesn't handle keywords args
139 cache = {}
139 cache = {}
140 if func.func_code.co_argcount == 1:
140 if func.func_code.co_argcount == 1:
141 # we gain a small amount of time because
141 # we gain a small amount of time because
142 # we don't need to pack/unpack the list
142 # we don't need to pack/unpack the list
143 def f(arg):
143 def f(arg):
144 if arg not in cache:
144 if arg not in cache:
145 cache[arg] = func(arg)
145 cache[arg] = func(arg)
146 return cache[arg]
146 return cache[arg]
147 else:
147 else:
148 def f(*args):
148 def f(*args):
149 if args not in cache:
149 if args not in cache:
150 cache[args] = func(*args)
150 cache[args] = func(*args)
151 return cache[args]
151 return cache[args]
152
152
153 return f
153 return f
154
154
155 def pipefilter(s, cmd):
155 def pipefilter(s, cmd):
156 '''filter string S through command CMD, returning its output'''
156 '''filter string S through command CMD, returning its output'''
157 (pout, pin) = popen2.popen2(cmd, -1, 'b')
157 (pout, pin) = popen2.popen2(cmd, -1, 'b')
158 def writer():
158 def writer():
159 try:
159 try:
160 pin.write(s)
160 pin.write(s)
161 pin.close()
161 pin.close()
162 except IOError, inst:
162 except IOError, inst:
163 if inst.errno != errno.EPIPE:
163 if inst.errno != errno.EPIPE:
164 raise
164 raise
165
165
166 # we should use select instead on UNIX, but this will work on most
166 # we should use select instead on UNIX, but this will work on most
167 # systems, including Windows
167 # systems, including Windows
168 w = threading.Thread(target=writer)
168 w = threading.Thread(target=writer)
169 w.start()
169 w.start()
170 f = pout.read()
170 f = pout.read()
171 pout.close()
171 pout.close()
172 w.join()
172 w.join()
173 return f
173 return f
174
174
175 def tempfilter(s, cmd):
175 def tempfilter(s, cmd):
176 '''filter string S through a pair of temporary files with CMD.
176 '''filter string S through a pair of temporary files with CMD.
177 CMD is used as a template to create the real command to be run,
177 CMD is used as a template to create the real command to be run,
178 with the strings INFILE and OUTFILE replaced by the real names of
178 with the strings INFILE and OUTFILE replaced by the real names of
179 the temporary files generated.'''
179 the temporary files generated.'''
180 inname, outname = None, None
180 inname, outname = None, None
181 try:
181 try:
182 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
182 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
183 fp = os.fdopen(infd, 'wb')
183 fp = os.fdopen(infd, 'wb')
184 fp.write(s)
184 fp.write(s)
185 fp.close()
185 fp.close()
186 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
186 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
187 os.close(outfd)
187 os.close(outfd)
188 cmd = cmd.replace('INFILE', inname)
188 cmd = cmd.replace('INFILE', inname)
189 cmd = cmd.replace('OUTFILE', outname)
189 cmd = cmd.replace('OUTFILE', outname)
190 code = os.system(cmd)
190 code = os.system(cmd)
191 if code: raise Abort(_("command '%s' failed: %s") %
191 if code: raise Abort(_("command '%s' failed: %s") %
192 (cmd, explain_exit(code)))
192 (cmd, explain_exit(code)))
193 return open(outname, 'rb').read()
193 return open(outname, 'rb').read()
194 finally:
194 finally:
195 try:
195 try:
196 if inname: os.unlink(inname)
196 if inname: os.unlink(inname)
197 except: pass
197 except: pass
198 try:
198 try:
199 if outname: os.unlink(outname)
199 if outname: os.unlink(outname)
200 except: pass
200 except: pass
201
201
202 filtertable = {
202 filtertable = {
203 'tempfile:': tempfilter,
203 'tempfile:': tempfilter,
204 'pipe:': pipefilter,
204 'pipe:': pipefilter,
205 }
205 }
206
206
207 def filter(s, cmd):
207 def filter(s, cmd):
208 "filter a string through a command that transforms its input to its output"
208 "filter a string through a command that transforms its input to its output"
209 for name, fn in filtertable.iteritems():
209 for name, fn in filtertable.iteritems():
210 if cmd.startswith(name):
210 if cmd.startswith(name):
211 return fn(s, cmd[len(name):].lstrip())
211 return fn(s, cmd[len(name):].lstrip())
212 return pipefilter(s, cmd)
212 return pipefilter(s, cmd)
213
213
214 def find_in_path(name, path, default=None):
214 def find_in_path(name, path, default=None):
215 '''find name in search path. path can be string (will be split
215 '''find name in search path. path can be string (will be split
216 with os.pathsep), or iterable thing that returns strings. if name
216 with os.pathsep), or iterable thing that returns strings. if name
217 found, return path to name. else return default.'''
217 found, return path to name. else return default.'''
218 if isinstance(path, str):
218 if isinstance(path, str):
219 path = path.split(os.pathsep)
219 path = path.split(os.pathsep)
220 for p in path:
220 for p in path:
221 p_name = os.path.join(p, name)
221 p_name = os.path.join(p, name)
222 if os.path.exists(p_name):
222 if os.path.exists(p_name):
223 return p_name
223 return p_name
224 return default
224 return default
225
225
226 def binary(s):
226 def binary(s):
227 """return true if a string is binary data using diff's heuristic"""
227 """return true if a string is binary data using diff's heuristic"""
228 if s and '\0' in s[:4096]:
228 if s and '\0' in s[:4096]:
229 return True
229 return True
230 return False
230 return False
231
231
232 def unique(g):
232 def unique(g):
233 """return the uniq elements of iterable g"""
233 """return the uniq elements of iterable g"""
234 seen = {}
234 seen = {}
235 l = []
235 l = []
236 for f in g:
236 for f in g:
237 if f not in seen:
237 if f not in seen:
238 seen[f] = 1
238 seen[f] = 1
239 l.append(f)
239 l.append(f)
240 return l
240 return l
241
241
242 class Abort(Exception):
242 class Abort(Exception):
243 """Raised if a command needs to print an error and exit."""
243 """Raised if a command needs to print an error and exit."""
244
244
245 class UnexpectedOutput(Abort):
245 class UnexpectedOutput(Abort):
246 """Raised to print an error with part of output and exit."""
246 """Raised to print an error with part of output and exit."""
247
247
248 def always(fn): return True
248 def always(fn): return True
249 def never(fn): return False
249 def never(fn): return False
250
250
251 def expand_glob(pats):
251 def expand_glob(pats):
252 '''On Windows, expand the implicit globs in a list of patterns'''
252 '''On Windows, expand the implicit globs in a list of patterns'''
253 if os.name != 'nt':
253 if os.name != 'nt':
254 return list(pats)
254 return list(pats)
255 ret = []
255 ret = []
256 for p in pats:
256 for p in pats:
257 kind, name = patkind(p, None)
257 kind, name = patkind(p, None)
258 if kind is None:
258 if kind is None:
259 globbed = glob.glob(name)
259 globbed = glob.glob(name)
260 if globbed:
260 if globbed:
261 ret.extend(globbed)
261 ret.extend(globbed)
262 continue
262 continue
263 # if we couldn't expand the glob, just keep it around
263 # if we couldn't expand the glob, just keep it around
264 ret.append(p)
264 ret.append(p)
265 return ret
265 return ret
266
266
267 def patkind(name, dflt_pat='glob'):
267 def patkind(name, dflt_pat='glob'):
268 """Split a string into an optional pattern kind prefix and the
268 """Split a string into an optional pattern kind prefix and the
269 actual pattern."""
269 actual pattern."""
270 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
270 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
271 if name.startswith(prefix + ':'): return name.split(':', 1)
271 if name.startswith(prefix + ':'): return name.split(':', 1)
272 return dflt_pat, name
272 return dflt_pat, name
273
273
274 def globre(pat, head='^', tail='$'):
274 def globre(pat, head='^', tail='$'):
275 "convert a glob pattern into a regexp"
275 "convert a glob pattern into a regexp"
276 i, n = 0, len(pat)
276 i, n = 0, len(pat)
277 res = ''
277 res = ''
278 group = False
278 group = False
279 def peek(): return i < n and pat[i]
279 def peek(): return i < n and pat[i]
280 while i < n:
280 while i < n:
281 c = pat[i]
281 c = pat[i]
282 i = i+1
282 i = i+1
283 if c == '*':
283 if c == '*':
284 if peek() == '*':
284 if peek() == '*':
285 i += 1
285 i += 1
286 res += '.*'
286 res += '.*'
287 else:
287 else:
288 res += '[^/]*'
288 res += '[^/]*'
289 elif c == '?':
289 elif c == '?':
290 res += '.'
290 res += '.'
291 elif c == '[':
291 elif c == '[':
292 j = i
292 j = i
293 if j < n and pat[j] in '!]':
293 if j < n and pat[j] in '!]':
294 j += 1
294 j += 1
295 while j < n and pat[j] != ']':
295 while j < n and pat[j] != ']':
296 j += 1
296 j += 1
297 if j >= n:
297 if j >= n:
298 res += '\\['
298 res += '\\['
299 else:
299 else:
300 stuff = pat[i:j].replace('\\','\\\\')
300 stuff = pat[i:j].replace('\\','\\\\')
301 i = j + 1
301 i = j + 1
302 if stuff[0] == '!':
302 if stuff[0] == '!':
303 stuff = '^' + stuff[1:]
303 stuff = '^' + stuff[1:]
304 elif stuff[0] == '^':
304 elif stuff[0] == '^':
305 stuff = '\\' + stuff
305 stuff = '\\' + stuff
306 res = '%s[%s]' % (res, stuff)
306 res = '%s[%s]' % (res, stuff)
307 elif c == '{':
307 elif c == '{':
308 group = True
308 group = True
309 res += '(?:'
309 res += '(?:'
310 elif c == '}' and group:
310 elif c == '}' and group:
311 res += ')'
311 res += ')'
312 group = False
312 group = False
313 elif c == ',' and group:
313 elif c == ',' and group:
314 res += '|'
314 res += '|'
315 elif c == '\\':
315 elif c == '\\':
316 p = peek()
316 p = peek()
317 if p:
317 if p:
318 i += 1
318 i += 1
319 res += re.escape(p)
319 res += re.escape(p)
320 else:
320 else:
321 res += re.escape(c)
321 res += re.escape(c)
322 else:
322 else:
323 res += re.escape(c)
323 res += re.escape(c)
324 return head + res + tail
324 return head + res + tail
325
325
326 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
326 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
327
327
328 def pathto(n1, n2):
328 def pathto(n1, n2):
329 '''return the relative path from one place to another.
329 '''return the relative path from one place to another.
330 n1 should use os.sep to separate directories
330 n1 should use os.sep to separate directories
331 n2 should use "/" to separate directories
331 n2 should use "/" to separate directories
332 returns an os.sep-separated path.
332 returns an os.sep-separated path.
333 '''
333 '''
334 if not n1: return localpath(n2)
334 if not n1: return localpath(n2)
335 a, b = n1.split(os.sep), n2.split('/')
335 a, b = n1.split(os.sep), n2.split('/')
336 a.reverse()
336 a.reverse()
337 b.reverse()
337 b.reverse()
338 while a and b and a[-1] == b[-1]:
338 while a and b and a[-1] == b[-1]:
339 a.pop()
339 a.pop()
340 b.pop()
340 b.pop()
341 b.reverse()
341 b.reverse()
342 return os.sep.join((['..'] * len(a)) + b)
342 return os.sep.join((['..'] * len(a)) + b)
343
343
344 def canonpath(root, cwd, myname):
344 def canonpath(root, cwd, myname):
345 """return the canonical path of myname, given cwd and root"""
345 """return the canonical path of myname, given cwd and root"""
346 if root == os.sep:
346 if root == os.sep:
347 rootsep = os.sep
347 rootsep = os.sep
348 elif root.endswith(os.sep):
348 elif root.endswith(os.sep):
349 rootsep = root
349 rootsep = root
350 else:
350 else:
351 rootsep = root + os.sep
351 rootsep = root + os.sep
352 name = myname
352 name = myname
353 if not os.path.isabs(name):
353 if not os.path.isabs(name):
354 name = os.path.join(root, cwd, name)
354 name = os.path.join(root, cwd, name)
355 name = os.path.normpath(name)
355 name = os.path.normpath(name)
356 if name != rootsep and name.startswith(rootsep):
356 if name != rootsep and name.startswith(rootsep):
357 name = name[len(rootsep):]
357 name = name[len(rootsep):]
358 audit_path(name)
358 audit_path(name)
359 return pconvert(name)
359 return pconvert(name)
360 elif name == root:
360 elif name == root:
361 return ''
361 return ''
362 else:
362 else:
363 # Determine whether `name' is in the hierarchy at or beneath `root',
363 # Determine whether `name' is in the hierarchy at or beneath `root',
364 # by iterating name=dirname(name) until that causes no change (can't
364 # by iterating name=dirname(name) until that causes no change (can't
365 # check name == '/', because that doesn't work on windows). For each
365 # check name == '/', because that doesn't work on windows). For each
366 # `name', compare dev/inode numbers. If they match, the list `rel'
366 # `name', compare dev/inode numbers. If they match, the list `rel'
367 # holds the reversed list of components making up the relative file
367 # holds the reversed list of components making up the relative file
368 # name we want.
368 # name we want.
369 root_st = os.stat(root)
369 root_st = os.stat(root)
370 rel = []
370 rel = []
371 while True:
371 while True:
372 try:
372 try:
373 name_st = os.stat(name)
373 name_st = os.stat(name)
374 except OSError:
374 except OSError:
375 break
375 break
376 if samestat(name_st, root_st):
376 if samestat(name_st, root_st):
377 if not rel:
378 # name was actually the same as root (maybe a symlink)
379 return ''
377 rel.reverse()
380 rel.reverse()
378 name = os.path.join(*rel)
381 name = os.path.join(*rel)
379 audit_path(name)
382 audit_path(name)
380 return pconvert(name)
383 return pconvert(name)
381 dirname, basename = os.path.split(name)
384 dirname, basename = os.path.split(name)
382 rel.append(basename)
385 rel.append(basename)
383 if dirname == name:
386 if dirname == name:
384 break
387 break
385 name = dirname
388 name = dirname
386
389
387 raise Abort('%s not under root' % myname)
390 raise Abort('%s not under root' % myname)
388
391
389 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
392 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
390 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
393 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
391
394
392 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='',
395 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='',
393 src=None, globbed=False):
396 src=None, globbed=False):
394 if not globbed:
397 if not globbed:
395 names = expand_glob(names)
398 names = expand_glob(names)
396 return _matcher(canonroot, cwd, names, inc, exc, head, 'relpath', src)
399 return _matcher(canonroot, cwd, names, inc, exc, head, 'relpath', src)
397
400
398 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
401 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
399 """build a function to match a set of file patterns
402 """build a function to match a set of file patterns
400
403
401 arguments:
404 arguments:
402 canonroot - the canonical root of the tree you're matching against
405 canonroot - the canonical root of the tree you're matching against
403 cwd - the current working directory, if relevant
406 cwd - the current working directory, if relevant
404 names - patterns to find
407 names - patterns to find
405 inc - patterns to include
408 inc - patterns to include
406 exc - patterns to exclude
409 exc - patterns to exclude
407 head - a regex to prepend to patterns to control whether a match is rooted
410 head - a regex to prepend to patterns to control whether a match is rooted
408
411
409 a pattern is one of:
412 a pattern is one of:
410 'glob:<rooted glob>'
413 'glob:<rooted glob>'
411 're:<rooted regexp>'
414 're:<rooted regexp>'
412 'path:<rooted path>'
415 'path:<rooted path>'
413 'relglob:<relative glob>'
416 'relglob:<relative glob>'
414 'relpath:<relative path>'
417 'relpath:<relative path>'
415 'relre:<relative regexp>'
418 'relre:<relative regexp>'
416 '<rooted path or regexp>'
419 '<rooted path or regexp>'
417
420
418 returns:
421 returns:
419 a 3-tuple containing
422 a 3-tuple containing
420 - list of explicit non-pattern names passed in
423 - list of explicit non-pattern names passed in
421 - a bool match(filename) function
424 - a bool match(filename) function
422 - a bool indicating if any patterns were passed in
425 - a bool indicating if any patterns were passed in
423
426
424 todo:
427 todo:
425 make head regex a rooted bool
428 make head regex a rooted bool
426 """
429 """
427
430
428 def contains_glob(name):
431 def contains_glob(name):
429 for c in name:
432 for c in name:
430 if c in _globchars: return True
433 if c in _globchars: return True
431 return False
434 return False
432
435
433 def regex(kind, name, tail):
436 def regex(kind, name, tail):
434 '''convert a pattern into a regular expression'''
437 '''convert a pattern into a regular expression'''
435 if kind == 're':
438 if kind == 're':
436 return name
439 return name
437 elif kind == 'path':
440 elif kind == 'path':
438 return '^' + re.escape(name) + '(?:/|$)'
441 return '^' + re.escape(name) + '(?:/|$)'
439 elif kind == 'relglob':
442 elif kind == 'relglob':
440 return head + globre(name, '(?:|.*/)', tail)
443 return head + globre(name, '(?:|.*/)', tail)
441 elif kind == 'relpath':
444 elif kind == 'relpath':
442 return head + re.escape(name) + tail
445 return head + re.escape(name) + tail
443 elif kind == 'relre':
446 elif kind == 'relre':
444 if name.startswith('^'):
447 if name.startswith('^'):
445 return name
448 return name
446 return '.*' + name
449 return '.*' + name
447 return head + globre(name, '', tail)
450 return head + globre(name, '', tail)
448
451
449 def matchfn(pats, tail):
452 def matchfn(pats, tail):
450 """build a matching function from a set of patterns"""
453 """build a matching function from a set of patterns"""
451 if not pats:
454 if not pats:
452 return
455 return
453 matches = []
456 matches = []
454 for k, p in pats:
457 for k, p in pats:
455 try:
458 try:
456 pat = '(?:%s)' % regex(k, p, tail)
459 pat = '(?:%s)' % regex(k, p, tail)
457 matches.append(re.compile(pat).match)
460 matches.append(re.compile(pat).match)
458 except re.error:
461 except re.error:
459 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
462 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
460 else: raise Abort("invalid pattern (%s): %s" % (k, p))
463 else: raise Abort("invalid pattern (%s): %s" % (k, p))
461
464
462 def buildfn(text):
465 def buildfn(text):
463 for m in matches:
466 for m in matches:
464 r = m(text)
467 r = m(text)
465 if r:
468 if r:
466 return r
469 return r
467
470
468 return buildfn
471 return buildfn
469
472
470 def globprefix(pat):
473 def globprefix(pat):
471 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
474 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
472 root = []
475 root = []
473 for p in pat.split(os.sep):
476 for p in pat.split(os.sep):
474 if contains_glob(p): break
477 if contains_glob(p): break
475 root.append(p)
478 root.append(p)
476 return '/'.join(root)
479 return '/'.join(root)
477
480
478 pats = []
481 pats = []
479 files = []
482 files = []
480 roots = []
483 roots = []
481 for kind, name in [patkind(p, dflt_pat) for p in names]:
484 for kind, name in [patkind(p, dflt_pat) for p in names]:
482 if kind in ('glob', 'relpath'):
485 if kind in ('glob', 'relpath'):
483 name = canonpath(canonroot, cwd, name)
486 name = canonpath(canonroot, cwd, name)
484 if name == '':
487 if name == '':
485 kind, name = 'glob', '**'
488 kind, name = 'glob', '**'
486 if kind in ('glob', 'path', 're'):
489 if kind in ('glob', 'path', 're'):
487 pats.append((kind, name))
490 pats.append((kind, name))
488 if kind == 'glob':
491 if kind == 'glob':
489 root = globprefix(name)
492 root = globprefix(name)
490 if root: roots.append(root)
493 if root: roots.append(root)
491 elif kind == 'relpath':
494 elif kind == 'relpath':
492 files.append((kind, name))
495 files.append((kind, name))
493 roots.append(name)
496 roots.append(name)
494
497
495 patmatch = matchfn(pats, '$') or always
498 patmatch = matchfn(pats, '$') or always
496 filematch = matchfn(files, '(?:/|$)') or always
499 filematch = matchfn(files, '(?:/|$)') or always
497 incmatch = always
500 incmatch = always
498 if inc:
501 if inc:
499 inckinds = [patkind(canonpath(canonroot, cwd, i)) for i in inc]
502 inckinds = [patkind(canonpath(canonroot, cwd, i)) for i in inc]
500 incmatch = matchfn(inckinds, '(?:/|$)')
503 incmatch = matchfn(inckinds, '(?:/|$)')
501 excmatch = lambda fn: False
504 excmatch = lambda fn: False
502 if exc:
505 if exc:
503 exckinds = [patkind(canonpath(canonroot, cwd, x)) for x in exc]
506 exckinds = [patkind(canonpath(canonroot, cwd, x)) for x in exc]
504 excmatch = matchfn(exckinds, '(?:/|$)')
507 excmatch = matchfn(exckinds, '(?:/|$)')
505
508
506 return (roots,
509 return (roots,
507 lambda fn: (incmatch(fn) and not excmatch(fn) and
510 lambda fn: (incmatch(fn) and not excmatch(fn) and
508 (fn.endswith('/') or
511 (fn.endswith('/') or
509 (not pats and not files) or
512 (not pats and not files) or
510 (pats and patmatch(fn)) or
513 (pats and patmatch(fn)) or
511 (files and filematch(fn)))),
514 (files and filematch(fn)))),
512 (inc or exc or (pats and pats != [('glob', '**')])) and True)
515 (inc or exc or (pats and pats != [('glob', '**')])) and True)
513
516
514 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
517 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
515 '''enhanced shell command execution.
518 '''enhanced shell command execution.
516 run with environment maybe modified, maybe in different dir.
519 run with environment maybe modified, maybe in different dir.
517
520
518 if command fails and onerr is None, return status. if ui object,
521 if command fails and onerr is None, return status. if ui object,
519 print error message and return status, else raise onerr object as
522 print error message and return status, else raise onerr object as
520 exception.'''
523 exception.'''
521 def py2shell(val):
524 def py2shell(val):
522 'convert python object into string that is useful to shell'
525 'convert python object into string that is useful to shell'
523 if val in (None, False):
526 if val in (None, False):
524 return '0'
527 return '0'
525 if val == True:
528 if val == True:
526 return '1'
529 return '1'
527 return str(val)
530 return str(val)
528 oldenv = {}
531 oldenv = {}
529 for k in environ:
532 for k in environ:
530 oldenv[k] = os.environ.get(k)
533 oldenv[k] = os.environ.get(k)
531 if cwd is not None:
534 if cwd is not None:
532 oldcwd = os.getcwd()
535 oldcwd = os.getcwd()
533 origcmd = cmd
536 origcmd = cmd
534 if os.name == 'nt':
537 if os.name == 'nt':
535 cmd = '"%s"' % cmd
538 cmd = '"%s"' % cmd
536 try:
539 try:
537 for k, v in environ.iteritems():
540 for k, v in environ.iteritems():
538 os.environ[k] = py2shell(v)
541 os.environ[k] = py2shell(v)
539 if cwd is not None and oldcwd != cwd:
542 if cwd is not None and oldcwd != cwd:
540 os.chdir(cwd)
543 os.chdir(cwd)
541 rc = os.system(cmd)
544 rc = os.system(cmd)
542 if rc and onerr:
545 if rc and onerr:
543 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
546 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
544 explain_exit(rc)[0])
547 explain_exit(rc)[0])
545 if errprefix:
548 if errprefix:
546 errmsg = '%s: %s' % (errprefix, errmsg)
549 errmsg = '%s: %s' % (errprefix, errmsg)
547 try:
550 try:
548 onerr.warn(errmsg + '\n')
551 onerr.warn(errmsg + '\n')
549 except AttributeError:
552 except AttributeError:
550 raise onerr(errmsg)
553 raise onerr(errmsg)
551 return rc
554 return rc
552 finally:
555 finally:
553 for k, v in oldenv.iteritems():
556 for k, v in oldenv.iteritems():
554 if v is None:
557 if v is None:
555 del os.environ[k]
558 del os.environ[k]
556 else:
559 else:
557 os.environ[k] = v
560 os.environ[k] = v
558 if cwd is not None and oldcwd != cwd:
561 if cwd is not None and oldcwd != cwd:
559 os.chdir(oldcwd)
562 os.chdir(oldcwd)
560
563
561 def rename(src, dst):
564 def rename(src, dst):
562 """forcibly rename a file"""
565 """forcibly rename a file"""
563 try:
566 try:
564 os.rename(src, dst)
567 os.rename(src, dst)
565 except OSError, err:
568 except OSError, err:
566 # on windows, rename to existing file is not allowed, so we
569 # on windows, rename to existing file is not allowed, so we
567 # must delete destination first. but if file is open, unlink
570 # must delete destination first. but if file is open, unlink
568 # schedules it for delete but does not delete it. rename
571 # schedules it for delete but does not delete it. rename
569 # happens immediately even for open files, so we create
572 # happens immediately even for open files, so we create
570 # temporary file, delete it, rename destination to that name,
573 # temporary file, delete it, rename destination to that name,
571 # then delete that. then rename is safe to do.
574 # then delete that. then rename is safe to do.
572 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
575 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
573 os.close(fd)
576 os.close(fd)
574 os.unlink(temp)
577 os.unlink(temp)
575 os.rename(dst, temp)
578 os.rename(dst, temp)
576 os.unlink(temp)
579 os.unlink(temp)
577 os.rename(src, dst)
580 os.rename(src, dst)
578
581
579 def unlink(f):
582 def unlink(f):
580 """unlink and remove the directory if it is empty"""
583 """unlink and remove the directory if it is empty"""
581 os.unlink(f)
584 os.unlink(f)
582 # try removing directories that might now be empty
585 # try removing directories that might now be empty
583 try:
586 try:
584 os.removedirs(os.path.dirname(f))
587 os.removedirs(os.path.dirname(f))
585 except OSError:
588 except OSError:
586 pass
589 pass
587
590
588 def copyfile(src, dest):
591 def copyfile(src, dest):
589 "copy a file, preserving mode"
592 "copy a file, preserving mode"
590 try:
593 try:
591 shutil.copyfile(src, dest)
594 shutil.copyfile(src, dest)
592 shutil.copymode(src, dest)
595 shutil.copymode(src, dest)
593 except shutil.Error, inst:
596 except shutil.Error, inst:
594 raise Abort(str(inst))
597 raise Abort(str(inst))
595
598
596 def copyfiles(src, dst, hardlink=None):
599 def copyfiles(src, dst, hardlink=None):
597 """Copy a directory tree using hardlinks if possible"""
600 """Copy a directory tree using hardlinks if possible"""
598
601
599 if hardlink is None:
602 if hardlink is None:
600 hardlink = (os.stat(src).st_dev ==
603 hardlink = (os.stat(src).st_dev ==
601 os.stat(os.path.dirname(dst)).st_dev)
604 os.stat(os.path.dirname(dst)).st_dev)
602
605
603 if os.path.isdir(src):
606 if os.path.isdir(src):
604 os.mkdir(dst)
607 os.mkdir(dst)
605 for name in os.listdir(src):
608 for name in os.listdir(src):
606 srcname = os.path.join(src, name)
609 srcname = os.path.join(src, name)
607 dstname = os.path.join(dst, name)
610 dstname = os.path.join(dst, name)
608 copyfiles(srcname, dstname, hardlink)
611 copyfiles(srcname, dstname, hardlink)
609 else:
612 else:
610 if hardlink:
613 if hardlink:
611 try:
614 try:
612 os_link(src, dst)
615 os_link(src, dst)
613 except (IOError, OSError):
616 except (IOError, OSError):
614 hardlink = False
617 hardlink = False
615 shutil.copy(src, dst)
618 shutil.copy(src, dst)
616 else:
619 else:
617 shutil.copy(src, dst)
620 shutil.copy(src, dst)
618
621
619 def audit_path(path):
622 def audit_path(path):
620 """Abort if path contains dangerous components"""
623 """Abort if path contains dangerous components"""
621 parts = os.path.normcase(path).split(os.sep)
624 parts = os.path.normcase(path).split(os.sep)
622 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
625 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
623 or os.pardir in parts):
626 or os.pardir in parts):
624 raise Abort(_("path contains illegal component: %s\n") % path)
627 raise Abort(_("path contains illegal component: %s\n") % path)
625
628
626 def _makelock_file(info, pathname):
629 def _makelock_file(info, pathname):
627 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
630 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
628 os.write(ld, info)
631 os.write(ld, info)
629 os.close(ld)
632 os.close(ld)
630
633
631 def _readlock_file(pathname):
634 def _readlock_file(pathname):
632 return posixfile(pathname).read()
635 return posixfile(pathname).read()
633
636
634 def nlinks(pathname):
637 def nlinks(pathname):
635 """Return number of hardlinks for the given file."""
638 """Return number of hardlinks for the given file."""
636 return os.lstat(pathname).st_nlink
639 return os.lstat(pathname).st_nlink
637
640
638 if hasattr(os, 'link'):
641 if hasattr(os, 'link'):
639 os_link = os.link
642 os_link = os.link
640 else:
643 else:
641 def os_link(src, dst):
644 def os_link(src, dst):
642 raise OSError(0, _("Hardlinks not supported"))
645 raise OSError(0, _("Hardlinks not supported"))
643
646
644 def fstat(fp):
647 def fstat(fp):
645 '''stat file object that may not have fileno method.'''
648 '''stat file object that may not have fileno method.'''
646 try:
649 try:
647 return os.fstat(fp.fileno())
650 return os.fstat(fp.fileno())
648 except AttributeError:
651 except AttributeError:
649 return os.stat(fp.name)
652 return os.stat(fp.name)
650
653
651 posixfile = file
654 posixfile = file
652
655
653 def is_win_9x():
656 def is_win_9x():
654 '''return true if run on windows 95, 98 or me.'''
657 '''return true if run on windows 95, 98 or me.'''
655 try:
658 try:
656 return sys.getwindowsversion()[3] == 1
659 return sys.getwindowsversion()[3] == 1
657 except AttributeError:
660 except AttributeError:
658 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
661 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
659
662
660 getuser_fallback = None
663 getuser_fallback = None
661
664
662 def getuser():
665 def getuser():
663 '''return name of current user'''
666 '''return name of current user'''
664 try:
667 try:
665 return getpass.getuser()
668 return getpass.getuser()
666 except ImportError:
669 except ImportError:
667 # import of pwd will fail on windows - try fallback
670 # import of pwd will fail on windows - try fallback
668 if getuser_fallback:
671 if getuser_fallback:
669 return getuser_fallback()
672 return getuser_fallback()
670 # raised if win32api not available
673 # raised if win32api not available
671 raise Abort(_('user name not available - set USERNAME '
674 raise Abort(_('user name not available - set USERNAME '
672 'environment variable'))
675 'environment variable'))
673
676
674 def username(uid=None):
677 def username(uid=None):
675 """Return the name of the user with the given uid.
678 """Return the name of the user with the given uid.
676
679
677 If uid is None, return the name of the current user."""
680 If uid is None, return the name of the current user."""
678 try:
681 try:
679 import pwd
682 import pwd
680 if uid is None:
683 if uid is None:
681 uid = os.getuid()
684 uid = os.getuid()
682 try:
685 try:
683 return pwd.getpwuid(uid)[0]
686 return pwd.getpwuid(uid)[0]
684 except KeyError:
687 except KeyError:
685 return str(uid)
688 return str(uid)
686 except ImportError:
689 except ImportError:
687 return None
690 return None
688
691
689 def groupname(gid=None):
692 def groupname(gid=None):
690 """Return the name of the group with the given gid.
693 """Return the name of the group with the given gid.
691
694
692 If gid is None, return the name of the current group."""
695 If gid is None, return the name of the current group."""
693 try:
696 try:
694 import grp
697 import grp
695 if gid is None:
698 if gid is None:
696 gid = os.getgid()
699 gid = os.getgid()
697 try:
700 try:
698 return grp.getgrgid(gid)[0]
701 return grp.getgrgid(gid)[0]
699 except KeyError:
702 except KeyError:
700 return str(gid)
703 return str(gid)
701 except ImportError:
704 except ImportError:
702 return None
705 return None
703
706
704 # File system features
707 # File system features
705
708
706 def checkfolding(path):
709 def checkfolding(path):
707 """
710 """
708 Check whether the given path is on a case-sensitive filesystem
711 Check whether the given path is on a case-sensitive filesystem
709
712
710 Requires a path (like /foo/.hg) ending with a foldable final
713 Requires a path (like /foo/.hg) ending with a foldable final
711 directory component.
714 directory component.
712 """
715 """
713 s1 = os.stat(path)
716 s1 = os.stat(path)
714 d, b = os.path.split(path)
717 d, b = os.path.split(path)
715 p2 = os.path.join(d, b.upper())
718 p2 = os.path.join(d, b.upper())
716 if path == p2:
719 if path == p2:
717 p2 = os.path.join(d, b.lower())
720 p2 = os.path.join(d, b.lower())
718 try:
721 try:
719 s2 = os.stat(p2)
722 s2 = os.stat(p2)
720 if s2 == s1:
723 if s2 == s1:
721 return False
724 return False
722 return True
725 return True
723 except:
726 except:
724 return True
727 return True
725
728
726 def checkexec(path):
729 def checkexec(path):
727 """
730 """
728 Check whether the given path is on a filesystem with UNIX-like exec flags
731 Check whether the given path is on a filesystem with UNIX-like exec flags
729
732
730 Requires a directory (like /foo/.hg)
733 Requires a directory (like /foo/.hg)
731 """
734 """
732 fh, fn = tempfile.mkstemp("", "", path)
735 fh, fn = tempfile.mkstemp("", "", path)
733 os.close(fh)
736 os.close(fh)
734 m = os.stat(fn).st_mode
737 m = os.stat(fn).st_mode
735 os.chmod(fn, m ^ 0111)
738 os.chmod(fn, m ^ 0111)
736 r = (os.stat(fn).st_mode != m)
739 r = (os.stat(fn).st_mode != m)
737 os.unlink(fn)
740 os.unlink(fn)
738 return r
741 return r
739
742
740 def execfunc(path, fallback):
743 def execfunc(path, fallback):
741 '''return an is_exec() function with default to fallback'''
744 '''return an is_exec() function with default to fallback'''
742 if checkexec(path):
745 if checkexec(path):
743 return lambda x: is_exec(os.path.join(path, x))
746 return lambda x: is_exec(os.path.join(path, x))
744 return fallback
747 return fallback
745
748
746 def checklink(path):
749 def checklink(path):
747 """check whether the given path is on a symlink-capable filesystem"""
750 """check whether the given path is on a symlink-capable filesystem"""
748 # mktemp is not racy because symlink creation will fail if the
751 # mktemp is not racy because symlink creation will fail if the
749 # file already exists
752 # file already exists
750 name = tempfile.mktemp(dir=path)
753 name = tempfile.mktemp(dir=path)
751 try:
754 try:
752 os.symlink(".", name)
755 os.symlink(".", name)
753 os.unlink(name)
756 os.unlink(name)
754 return True
757 return True
755 except (OSError, AttributeError):
758 except (OSError, AttributeError):
756 return False
759 return False
757
760
758 def linkfunc(path, fallback):
761 def linkfunc(path, fallback):
759 '''return an is_link() function with default to fallback'''
762 '''return an is_link() function with default to fallback'''
760 if checklink(path):
763 if checklink(path):
761 return lambda x: is_link(os.path.join(path, x))
764 return lambda x: is_link(os.path.join(path, x))
762 return fallback
765 return fallback
763
766
764 # Platform specific variants
767 # Platform specific variants
765 if os.name == 'nt':
768 if os.name == 'nt':
766 import msvcrt
769 import msvcrt
767 nulldev = 'NUL:'
770 nulldev = 'NUL:'
768
771
769 class winstdout:
772 class winstdout:
770 '''stdout on windows misbehaves if sent through a pipe'''
773 '''stdout on windows misbehaves if sent through a pipe'''
771
774
772 def __init__(self, fp):
775 def __init__(self, fp):
773 self.fp = fp
776 self.fp = fp
774
777
775 def __getattr__(self, key):
778 def __getattr__(self, key):
776 return getattr(self.fp, key)
779 return getattr(self.fp, key)
777
780
778 def close(self):
781 def close(self):
779 try:
782 try:
780 self.fp.close()
783 self.fp.close()
781 except: pass
784 except: pass
782
785
783 def write(self, s):
786 def write(self, s):
784 try:
787 try:
785 return self.fp.write(s)
788 return self.fp.write(s)
786 except IOError, inst:
789 except IOError, inst:
787 if inst.errno != 0: raise
790 if inst.errno != 0: raise
788 self.close()
791 self.close()
789 raise IOError(errno.EPIPE, 'Broken pipe')
792 raise IOError(errno.EPIPE, 'Broken pipe')
790
793
791 sys.stdout = winstdout(sys.stdout)
794 sys.stdout = winstdout(sys.stdout)
792
795
793 def system_rcpath():
796 def system_rcpath():
794 try:
797 try:
795 return system_rcpath_win32()
798 return system_rcpath_win32()
796 except:
799 except:
797 return [r'c:\mercurial\mercurial.ini']
800 return [r'c:\mercurial\mercurial.ini']
798
801
799 def os_rcpath():
802 def os_rcpath():
800 '''return default os-specific hgrc search path'''
803 '''return default os-specific hgrc search path'''
801 path = system_rcpath()
804 path = system_rcpath()
802 path.extend(user_rcpath())
805 path.extend(user_rcpath())
803 path = [os.path.normpath(f) for f in path]
806 path = [os.path.normpath(f) for f in path]
804 return path
807 return path
805
808
806 def user_rcpath():
809 def user_rcpath():
807 '''return os-specific hgrc search path to the user dir'''
810 '''return os-specific hgrc search path to the user dir'''
808 path = [os.path.join(os.path.expanduser('~'), 'mercurial.ini')]
811 path = [os.path.join(os.path.expanduser('~'), 'mercurial.ini')]
809 userprofile = os.environ.get('USERPROFILE')
812 userprofile = os.environ.get('USERPROFILE')
810 if userprofile:
813 if userprofile:
811 path.append(os.path.join(userprofile, 'mercurial.ini'))
814 path.append(os.path.join(userprofile, 'mercurial.ini'))
812 return path
815 return path
813
816
814 def parse_patch_output(output_line):
817 def parse_patch_output(output_line):
815 """parses the output produced by patch and returns the file name"""
818 """parses the output produced by patch and returns the file name"""
816 pf = output_line[14:]
819 pf = output_line[14:]
817 if pf[0] == '`':
820 if pf[0] == '`':
818 pf = pf[1:-1] # Remove the quotes
821 pf = pf[1:-1] # Remove the quotes
819 return pf
822 return pf
820
823
821 def testpid(pid):
824 def testpid(pid):
822 '''return False if pid dead, True if running or not known'''
825 '''return False if pid dead, True if running or not known'''
823 return True
826 return True
824
827
825 def set_exec(f, mode):
828 def set_exec(f, mode):
826 pass
829 pass
827
830
828 def set_link(f, mode):
831 def set_link(f, mode):
829 pass
832 pass
830
833
831 def set_binary(fd):
834 def set_binary(fd):
832 msvcrt.setmode(fd.fileno(), os.O_BINARY)
835 msvcrt.setmode(fd.fileno(), os.O_BINARY)
833
836
834 def pconvert(path):
837 def pconvert(path):
835 return path.replace("\\", "/")
838 return path.replace("\\", "/")
836
839
837 def localpath(path):
840 def localpath(path):
838 return path.replace('/', '\\')
841 return path.replace('/', '\\')
839
842
840 def normpath(path):
843 def normpath(path):
841 return pconvert(os.path.normpath(path))
844 return pconvert(os.path.normpath(path))
842
845
843 makelock = _makelock_file
846 makelock = _makelock_file
844 readlock = _readlock_file
847 readlock = _readlock_file
845
848
846 def samestat(s1, s2):
849 def samestat(s1, s2):
847 return False
850 return False
848
851
852 # A sequence of backslashes is special iff it precedes a double quote:
853 # - if there's an even number of backslashes, the double quote is not
854 # quoted (i.e. it ends the quoted region)
855 # - if there's an odd number of backslashes, the double quote is quoted
856 # - in both cases, every pair of backslashes is unquoted into a single
857 # backslash
858 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
859 # So, to quote a string, we must surround it in double quotes, double
860 # the number of backslashes that preceed double quotes and add another
861 # backslash before every double quote (being careful with the double
862 # quote we've appended to the end)
863 _quotere = None
849 def shellquote(s):
864 def shellquote(s):
850 return '"%s"' % s.replace('"', '\\"')
865 global _quotere
866 if _quotere is None:
867 _quotere = re.compile(r'(\\*)("|\\$)')
868 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
851
869
852 def explain_exit(code):
870 def explain_exit(code):
853 return _("exited with status %d") % code, code
871 return _("exited with status %d") % code, code
854
872
855 # if you change this stub into a real check, please try to implement the
873 # if you change this stub into a real check, please try to implement the
856 # username and groupname functions above, too.
874 # username and groupname functions above, too.
857 def isowner(fp, st=None):
875 def isowner(fp, st=None):
858 return True
876 return True
859
877
860 try:
878 try:
861 # override functions with win32 versions if possible
879 # override functions with win32 versions if possible
862 from util_win32 import *
880 from util_win32 import *
863 if not is_win_9x():
881 if not is_win_9x():
864 posixfile = posixfile_nt
882 posixfile = posixfile_nt
865 except ImportError:
883 except ImportError:
866 pass
884 pass
867
885
868 else:
886 else:
869 nulldev = '/dev/null'
887 nulldev = '/dev/null'
870 _umask = os.umask(0)
888 _umask = os.umask(0)
871 os.umask(_umask)
889 os.umask(_umask)
872
890
873 def rcfiles(path):
891 def rcfiles(path):
874 rcs = [os.path.join(path, 'hgrc')]
892 rcs = [os.path.join(path, 'hgrc')]
875 rcdir = os.path.join(path, 'hgrc.d')
893 rcdir = os.path.join(path, 'hgrc.d')
876 try:
894 try:
877 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
895 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
878 if f.endswith(".rc")])
896 if f.endswith(".rc")])
879 except OSError:
897 except OSError:
880 pass
898 pass
881 return rcs
899 return rcs
882
900
883 def os_rcpath():
901 def os_rcpath():
884 '''return default os-specific hgrc search path'''
902 '''return default os-specific hgrc search path'''
885 path = system_rcpath()
903 path = system_rcpath()
886 path.extend(user_rcpath())
904 path.extend(user_rcpath())
887 path = [os.path.normpath(f) for f in path]
905 path = [os.path.normpath(f) for f in path]
888 return path
906 return path
889
907
890 def system_rcpath():
908 def system_rcpath():
891 path = []
909 path = []
892 # old mod_python does not set sys.argv
910 # old mod_python does not set sys.argv
893 if len(getattr(sys, 'argv', [])) > 0:
911 if len(getattr(sys, 'argv', [])) > 0:
894 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
912 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
895 '/../etc/mercurial'))
913 '/../etc/mercurial'))
896 path.extend(rcfiles('/etc/mercurial'))
914 path.extend(rcfiles('/etc/mercurial'))
897 return path
915 return path
898
916
899 def user_rcpath():
917 def user_rcpath():
900 return [os.path.expanduser('~/.hgrc')]
918 return [os.path.expanduser('~/.hgrc')]
901
919
902 def parse_patch_output(output_line):
920 def parse_patch_output(output_line):
903 """parses the output produced by patch and returns the file name"""
921 """parses the output produced by patch and returns the file name"""
904 pf = output_line[14:]
922 pf = output_line[14:]
905 if pf.startswith("'") and pf.endswith("'") and " " in pf:
923 if pf.startswith("'") and pf.endswith("'") and " " in pf:
906 pf = pf[1:-1] # Remove the quotes
924 pf = pf[1:-1] # Remove the quotes
907 return pf
925 return pf
908
926
909 def is_exec(f):
927 def is_exec(f):
910 """check whether a file is executable"""
928 """check whether a file is executable"""
911 return (os.lstat(f).st_mode & 0100 != 0)
929 return (os.lstat(f).st_mode & 0100 != 0)
912
930
913 def set_exec(f, mode):
931 def set_exec(f, mode):
914 s = os.lstat(f).st_mode
932 s = os.lstat(f).st_mode
915 if (s & 0100 != 0) == mode:
933 if (s & 0100 != 0) == mode:
916 return
934 return
917 if mode:
935 if mode:
918 # Turn on +x for every +r bit when making a file executable
936 # Turn on +x for every +r bit when making a file executable
919 # and obey umask.
937 # and obey umask.
920 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
938 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
921 else:
939 else:
922 os.chmod(f, s & 0666)
940 os.chmod(f, s & 0666)
923
941
924 def is_link(f):
942 def is_link(f):
925 """check whether a file is a symlink"""
943 """check whether a file is a symlink"""
926 return (os.lstat(f).st_mode & 0120000 == 0120000)
944 return (os.lstat(f).st_mode & 0120000 == 0120000)
927
945
928 def set_link(f, mode):
946 def set_link(f, mode):
929 """make a file a symbolic link/regular file
947 """make a file a symbolic link/regular file
930
948
931 if a file is changed to a link, its contents become the link data
949 if a file is changed to a link, its contents become the link data
932 if a link is changed to a file, its link data become its contents
950 if a link is changed to a file, its link data become its contents
933 """
951 """
934
952
935 m = is_link(f)
953 m = is_link(f)
936 if m == bool(mode):
954 if m == bool(mode):
937 return
955 return
938
956
939 if mode: # switch file to link
957 if mode: # switch file to link
940 data = file(f).read()
958 data = file(f).read()
941 os.unlink(f)
959 os.unlink(f)
942 os.symlink(data, f)
960 os.symlink(data, f)
943 else:
961 else:
944 data = os.readlink(f)
962 data = os.readlink(f)
945 os.unlink(f)
963 os.unlink(f)
946 file(f, "w").write(data)
964 file(f, "w").write(data)
947
965
948 def set_binary(fd):
966 def set_binary(fd):
949 pass
967 pass
950
968
951 def pconvert(path):
969 def pconvert(path):
952 return path
970 return path
953
971
954 def localpath(path):
972 def localpath(path):
955 return path
973 return path
956
974
957 normpath = os.path.normpath
975 normpath = os.path.normpath
958 samestat = os.path.samestat
976 samestat = os.path.samestat
959
977
960 def makelock(info, pathname):
978 def makelock(info, pathname):
961 try:
979 try:
962 os.symlink(info, pathname)
980 os.symlink(info, pathname)
963 except OSError, why:
981 except OSError, why:
964 if why.errno == errno.EEXIST:
982 if why.errno == errno.EEXIST:
965 raise
983 raise
966 else:
984 else:
967 _makelock_file(info, pathname)
985 _makelock_file(info, pathname)
968
986
969 def readlock(pathname):
987 def readlock(pathname):
970 try:
988 try:
971 return os.readlink(pathname)
989 return os.readlink(pathname)
972 except OSError, why:
990 except OSError, why:
973 if why.errno == errno.EINVAL:
991 if why.errno == errno.EINVAL:
974 return _readlock_file(pathname)
992 return _readlock_file(pathname)
975 else:
993 else:
976 raise
994 raise
977
995
978 def shellquote(s):
996 def shellquote(s):
979 return "'%s'" % s.replace("'", "'\\''")
997 return "'%s'" % s.replace("'", "'\\''")
980
998
981 def testpid(pid):
999 def testpid(pid):
982 '''return False if pid dead, True if running or not sure'''
1000 '''return False if pid dead, True if running or not sure'''
983 try:
1001 try:
984 os.kill(pid, 0)
1002 os.kill(pid, 0)
985 return True
1003 return True
986 except OSError, inst:
1004 except OSError, inst:
987 return inst.errno != errno.ESRCH
1005 return inst.errno != errno.ESRCH
988
1006
989 def explain_exit(code):
1007 def explain_exit(code):
990 """return a 2-tuple (desc, code) describing a process's status"""
1008 """return a 2-tuple (desc, code) describing a process's status"""
991 if os.WIFEXITED(code):
1009 if os.WIFEXITED(code):
992 val = os.WEXITSTATUS(code)
1010 val = os.WEXITSTATUS(code)
993 return _("exited with status %d") % val, val
1011 return _("exited with status %d") % val, val
994 elif os.WIFSIGNALED(code):
1012 elif os.WIFSIGNALED(code):
995 val = os.WTERMSIG(code)
1013 val = os.WTERMSIG(code)
996 return _("killed by signal %d") % val, val
1014 return _("killed by signal %d") % val, val
997 elif os.WIFSTOPPED(code):
1015 elif os.WIFSTOPPED(code):
998 val = os.WSTOPSIG(code)
1016 val = os.WSTOPSIG(code)
999 return _("stopped by signal %d") % val, val
1017 return _("stopped by signal %d") % val, val
1000 raise ValueError(_("invalid exit code"))
1018 raise ValueError(_("invalid exit code"))
1001
1019
1002 def isowner(fp, st=None):
1020 def isowner(fp, st=None):
1003 """Return True if the file object f belongs to the current user.
1021 """Return True if the file object f belongs to the current user.
1004
1022
1005 The return value of a util.fstat(f) may be passed as the st argument.
1023 The return value of a util.fstat(f) may be passed as the st argument.
1006 """
1024 """
1007 if st is None:
1025 if st is None:
1008 st = fstat(fp)
1026 st = fstat(fp)
1009 return st.st_uid == os.getuid()
1027 return st.st_uid == os.getuid()
1010
1028
1011 def _buildencodefun():
1029 def _buildencodefun():
1012 e = '_'
1030 e = '_'
1013 win_reserved = [ord(x) for x in '\\:*?"<>|']
1031 win_reserved = [ord(x) for x in '\\:*?"<>|']
1014 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1032 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1015 for x in (range(32) + range(126, 256) + win_reserved):
1033 for x in (range(32) + range(126, 256) + win_reserved):
1016 cmap[chr(x)] = "~%02x" % x
1034 cmap[chr(x)] = "~%02x" % x
1017 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1035 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1018 cmap[chr(x)] = e + chr(x).lower()
1036 cmap[chr(x)] = e + chr(x).lower()
1019 dmap = {}
1037 dmap = {}
1020 for k, v in cmap.iteritems():
1038 for k, v in cmap.iteritems():
1021 dmap[v] = k
1039 dmap[v] = k
1022 def decode(s):
1040 def decode(s):
1023 i = 0
1041 i = 0
1024 while i < len(s):
1042 while i < len(s):
1025 for l in xrange(1, 4):
1043 for l in xrange(1, 4):
1026 try:
1044 try:
1027 yield dmap[s[i:i+l]]
1045 yield dmap[s[i:i+l]]
1028 i += l
1046 i += l
1029 break
1047 break
1030 except KeyError:
1048 except KeyError:
1031 pass
1049 pass
1032 else:
1050 else:
1033 raise KeyError
1051 raise KeyError
1034 return (lambda s: "".join([cmap[c] for c in s]),
1052 return (lambda s: "".join([cmap[c] for c in s]),
1035 lambda s: "".join(list(decode(s))))
1053 lambda s: "".join(list(decode(s))))
1036
1054
1037 encodefilename, decodefilename = _buildencodefun()
1055 encodefilename, decodefilename = _buildencodefun()
1038
1056
1039 def encodedopener(openerfn, fn):
1057 def encodedopener(openerfn, fn):
1040 def o(path, *args, **kw):
1058 def o(path, *args, **kw):
1041 return openerfn(fn(path), *args, **kw)
1059 return openerfn(fn(path), *args, **kw)
1042 return o
1060 return o
1043
1061
1044 def opener(base, audit=True):
1062 def opener(base, audit=True):
1045 """
1063 """
1046 return a function that opens files relative to base
1064 return a function that opens files relative to base
1047
1065
1048 this function is used to hide the details of COW semantics and
1066 this function is used to hide the details of COW semantics and
1049 remote file access from higher level code.
1067 remote file access from higher level code.
1050 """
1068 """
1051 p = base
1069 p = base
1052 audit_p = audit
1070 audit_p = audit
1053
1071
1054 def mktempcopy(name):
1072 def mktempcopy(name):
1055 d, fn = os.path.split(name)
1073 d, fn = os.path.split(name)
1056 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1074 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1057 os.close(fd)
1075 os.close(fd)
1058 ofp = posixfile(temp, "wb")
1076 ofp = posixfile(temp, "wb")
1059 try:
1077 try:
1060 try:
1078 try:
1061 ifp = posixfile(name, "rb")
1079 ifp = posixfile(name, "rb")
1062 except IOError, inst:
1080 except IOError, inst:
1063 if not getattr(inst, 'filename', None):
1081 if not getattr(inst, 'filename', None):
1064 inst.filename = name
1082 inst.filename = name
1065 raise
1083 raise
1066 for chunk in filechunkiter(ifp):
1084 for chunk in filechunkiter(ifp):
1067 ofp.write(chunk)
1085 ofp.write(chunk)
1068 ifp.close()
1086 ifp.close()
1069 ofp.close()
1087 ofp.close()
1070 except:
1088 except:
1071 try: os.unlink(temp)
1089 try: os.unlink(temp)
1072 except: pass
1090 except: pass
1073 raise
1091 raise
1074 st = os.lstat(name)
1092 st = os.lstat(name)
1075 os.chmod(temp, st.st_mode)
1093 os.chmod(temp, st.st_mode)
1076 return temp
1094 return temp
1077
1095
1078 class atomictempfile(posixfile):
1096 class atomictempfile(posixfile):
1079 """the file will only be copied when rename is called"""
1097 """the file will only be copied when rename is called"""
1080 def __init__(self, name, mode):
1098 def __init__(self, name, mode):
1081 self.__name = name
1099 self.__name = name
1082 self.temp = mktempcopy(name)
1100 self.temp = mktempcopy(name)
1083 posixfile.__init__(self, self.temp, mode)
1101 posixfile.__init__(self, self.temp, mode)
1084 def rename(self):
1102 def rename(self):
1085 if not self.closed:
1103 if not self.closed:
1086 posixfile.close(self)
1104 posixfile.close(self)
1087 rename(self.temp, localpath(self.__name))
1105 rename(self.temp, localpath(self.__name))
1088 def __del__(self):
1106 def __del__(self):
1089 if not self.closed:
1107 if not self.closed:
1090 try:
1108 try:
1091 os.unlink(self.temp)
1109 os.unlink(self.temp)
1092 except: pass
1110 except: pass
1093 posixfile.close(self)
1111 posixfile.close(self)
1094
1112
1095 class atomicfile(atomictempfile):
1113 class atomicfile(atomictempfile):
1096 """the file will only be copied on close"""
1114 """the file will only be copied on close"""
1097 def __init__(self, name, mode):
1115 def __init__(self, name, mode):
1098 atomictempfile.__init__(self, name, mode)
1116 atomictempfile.__init__(self, name, mode)
1099 def close(self):
1117 def close(self):
1100 self.rename()
1118 self.rename()
1101 def __del__(self):
1119 def __del__(self):
1102 self.rename()
1120 self.rename()
1103
1121
1104 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
1122 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
1105 if audit_p:
1123 if audit_p:
1106 audit_path(path)
1124 audit_path(path)
1107 f = os.path.join(p, path)
1125 f = os.path.join(p, path)
1108
1126
1109 if not text:
1127 if not text:
1110 mode += "b" # for that other OS
1128 mode += "b" # for that other OS
1111
1129
1112 if mode[0] != "r":
1130 if mode[0] != "r":
1113 try:
1131 try:
1114 nlink = nlinks(f)
1132 nlink = nlinks(f)
1115 except OSError:
1133 except OSError:
1116 d = os.path.dirname(f)
1134 d = os.path.dirname(f)
1117 if not os.path.isdir(d):
1135 if not os.path.isdir(d):
1118 os.makedirs(d)
1136 os.makedirs(d)
1119 else:
1137 else:
1120 if atomic:
1138 if atomic:
1121 return atomicfile(f, mode)
1139 return atomicfile(f, mode)
1122 elif atomictemp:
1140 elif atomictemp:
1123 return atomictempfile(f, mode)
1141 return atomictempfile(f, mode)
1124 if nlink > 1:
1142 if nlink > 1:
1125 rename(mktempcopy(f), f)
1143 rename(mktempcopy(f), f)
1126 return posixfile(f, mode)
1144 return posixfile(f, mode)
1127
1145
1128 return o
1146 return o
1129
1147
1130 class chunkbuffer(object):
1148 class chunkbuffer(object):
1131 """Allow arbitrary sized chunks of data to be efficiently read from an
1149 """Allow arbitrary sized chunks of data to be efficiently read from an
1132 iterator over chunks of arbitrary size."""
1150 iterator over chunks of arbitrary size."""
1133
1151
1134 def __init__(self, in_iter, targetsize = 2**16):
1152 def __init__(self, in_iter, targetsize = 2**16):
1135 """in_iter is the iterator that's iterating over the input chunks.
1153 """in_iter is the iterator that's iterating over the input chunks.
1136 targetsize is how big a buffer to try to maintain."""
1154 targetsize is how big a buffer to try to maintain."""
1137 self.in_iter = iter(in_iter)
1155 self.in_iter = iter(in_iter)
1138 self.buf = ''
1156 self.buf = ''
1139 self.targetsize = int(targetsize)
1157 self.targetsize = int(targetsize)
1140 if self.targetsize <= 0:
1158 if self.targetsize <= 0:
1141 raise ValueError(_("targetsize must be greater than 0, was %d") %
1159 raise ValueError(_("targetsize must be greater than 0, was %d") %
1142 targetsize)
1160 targetsize)
1143 self.iterempty = False
1161 self.iterempty = False
1144
1162
1145 def fillbuf(self):
1163 def fillbuf(self):
1146 """Ignore target size; read every chunk from iterator until empty."""
1164 """Ignore target size; read every chunk from iterator until empty."""
1147 if not self.iterempty:
1165 if not self.iterempty:
1148 collector = cStringIO.StringIO()
1166 collector = cStringIO.StringIO()
1149 collector.write(self.buf)
1167 collector.write(self.buf)
1150 for ch in self.in_iter:
1168 for ch in self.in_iter:
1151 collector.write(ch)
1169 collector.write(ch)
1152 self.buf = collector.getvalue()
1170 self.buf = collector.getvalue()
1153 self.iterempty = True
1171 self.iterempty = True
1154
1172
1155 def read(self, l):
1173 def read(self, l):
1156 """Read L bytes of data from the iterator of chunks of data.
1174 """Read L bytes of data from the iterator of chunks of data.
1157 Returns less than L bytes if the iterator runs dry."""
1175 Returns less than L bytes if the iterator runs dry."""
1158 if l > len(self.buf) and not self.iterempty:
1176 if l > len(self.buf) and not self.iterempty:
1159 # Clamp to a multiple of self.targetsize
1177 # Clamp to a multiple of self.targetsize
1160 targetsize = self.targetsize * ((l // self.targetsize) + 1)
1178 targetsize = self.targetsize * ((l // self.targetsize) + 1)
1161 collector = cStringIO.StringIO()
1179 collector = cStringIO.StringIO()
1162 collector.write(self.buf)
1180 collector.write(self.buf)
1163 collected = len(self.buf)
1181 collected = len(self.buf)
1164 for chunk in self.in_iter:
1182 for chunk in self.in_iter:
1165 collector.write(chunk)
1183 collector.write(chunk)
1166 collected += len(chunk)
1184 collected += len(chunk)
1167 if collected >= targetsize:
1185 if collected >= targetsize:
1168 break
1186 break
1169 if collected < targetsize:
1187 if collected < targetsize:
1170 self.iterempty = True
1188 self.iterempty = True
1171 self.buf = collector.getvalue()
1189 self.buf = collector.getvalue()
1172 s, self.buf = self.buf[:l], buffer(self.buf, l)
1190 s, self.buf = self.buf[:l], buffer(self.buf, l)
1173 return s
1191 return s
1174
1192
1175 def filechunkiter(f, size=65536, limit=None):
1193 def filechunkiter(f, size=65536, limit=None):
1176 """Create a generator that produces the data in the file size
1194 """Create a generator that produces the data in the file size
1177 (default 65536) bytes at a time, up to optional limit (default is
1195 (default 65536) bytes at a time, up to optional limit (default is
1178 to read all data). Chunks may be less than size bytes if the
1196 to read all data). Chunks may be less than size bytes if the
1179 chunk is the last chunk in the file, or the file is a socket or
1197 chunk is the last chunk in the file, or the file is a socket or
1180 some other type of file that sometimes reads less data than is
1198 some other type of file that sometimes reads less data than is
1181 requested."""
1199 requested."""
1182 assert size >= 0
1200 assert size >= 0
1183 assert limit is None or limit >= 0
1201 assert limit is None or limit >= 0
1184 while True:
1202 while True:
1185 if limit is None: nbytes = size
1203 if limit is None: nbytes = size
1186 else: nbytes = min(limit, size)
1204 else: nbytes = min(limit, size)
1187 s = nbytes and f.read(nbytes)
1205 s = nbytes and f.read(nbytes)
1188 if not s: break
1206 if not s: break
1189 if limit: limit -= len(s)
1207 if limit: limit -= len(s)
1190 yield s
1208 yield s
1191
1209
1192 def makedate():
1210 def makedate():
1193 lt = time.localtime()
1211 lt = time.localtime()
1194 if lt[8] == 1 and time.daylight:
1212 if lt[8] == 1 and time.daylight:
1195 tz = time.altzone
1213 tz = time.altzone
1196 else:
1214 else:
1197 tz = time.timezone
1215 tz = time.timezone
1198 return time.mktime(lt), tz
1216 return time.mktime(lt), tz
1199
1217
1200 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1218 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1201 """represent a (unixtime, offset) tuple as a localized time.
1219 """represent a (unixtime, offset) tuple as a localized time.
1202 unixtime is seconds since the epoch, and offset is the time zone's
1220 unixtime is seconds since the epoch, and offset is the time zone's
1203 number of seconds away from UTC. if timezone is false, do not
1221 number of seconds away from UTC. if timezone is false, do not
1204 append time zone to string."""
1222 append time zone to string."""
1205 t, tz = date or makedate()
1223 t, tz = date or makedate()
1206 s = time.strftime(format, time.gmtime(float(t) - tz))
1224 s = time.strftime(format, time.gmtime(float(t) - tz))
1207 if timezone:
1225 if timezone:
1208 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1226 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1209 return s
1227 return s
1210
1228
1211 def strdate(string, format, defaults):
1229 def strdate(string, format, defaults):
1212 """parse a localized time string and return a (unixtime, offset) tuple.
1230 """parse a localized time string and return a (unixtime, offset) tuple.
1213 if the string cannot be parsed, ValueError is raised."""
1231 if the string cannot be parsed, ValueError is raised."""
1214 def timezone(string):
1232 def timezone(string):
1215 tz = string.split()[-1]
1233 tz = string.split()[-1]
1216 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1234 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1217 tz = int(tz)
1235 tz = int(tz)
1218 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1236 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1219 return offset
1237 return offset
1220 if tz == "GMT" or tz == "UTC":
1238 if tz == "GMT" or tz == "UTC":
1221 return 0
1239 return 0
1222 return None
1240 return None
1223
1241
1224 # NOTE: unixtime = localunixtime + offset
1242 # NOTE: unixtime = localunixtime + offset
1225 offset, date = timezone(string), string
1243 offset, date = timezone(string), string
1226 if offset != None:
1244 if offset != None:
1227 date = " ".join(string.split()[:-1])
1245 date = " ".join(string.split()[:-1])
1228
1246
1229 # add missing elements from defaults
1247 # add missing elements from defaults
1230 for part in defaults:
1248 for part in defaults:
1231 found = [True for p in part if ("%"+p) in format]
1249 found = [True for p in part if ("%"+p) in format]
1232 if not found:
1250 if not found:
1233 date += "@" + defaults[part]
1251 date += "@" + defaults[part]
1234 format += "@%" + part[0]
1252 format += "@%" + part[0]
1235
1253
1236 timetuple = time.strptime(date, format)
1254 timetuple = time.strptime(date, format)
1237 localunixtime = int(calendar.timegm(timetuple))
1255 localunixtime = int(calendar.timegm(timetuple))
1238 if offset is None:
1256 if offset is None:
1239 # local timezone
1257 # local timezone
1240 unixtime = int(time.mktime(timetuple))
1258 unixtime = int(time.mktime(timetuple))
1241 offset = unixtime - localunixtime
1259 offset = unixtime - localunixtime
1242 else:
1260 else:
1243 unixtime = localunixtime + offset
1261 unixtime = localunixtime + offset
1244 return unixtime, offset
1262 return unixtime, offset
1245
1263
1246 def parsedate(string, formats=None, defaults=None):
1264 def parsedate(string, formats=None, defaults=None):
1247 """parse a localized time string and return a (unixtime, offset) tuple.
1265 """parse a localized time string and return a (unixtime, offset) tuple.
1248 The date may be a "unixtime offset" string or in one of the specified
1266 The date may be a "unixtime offset" string or in one of the specified
1249 formats."""
1267 formats."""
1250 if not string:
1268 if not string:
1251 return 0, 0
1269 return 0, 0
1252 if not formats:
1270 if not formats:
1253 formats = defaultdateformats
1271 formats = defaultdateformats
1254 string = string.strip()
1272 string = string.strip()
1255 try:
1273 try:
1256 when, offset = map(int, string.split(' '))
1274 when, offset = map(int, string.split(' '))
1257 except ValueError:
1275 except ValueError:
1258 # fill out defaults
1276 # fill out defaults
1259 if not defaults:
1277 if not defaults:
1260 defaults = {}
1278 defaults = {}
1261 now = makedate()
1279 now = makedate()
1262 for part in "d mb yY HI M S".split():
1280 for part in "d mb yY HI M S".split():
1263 if part not in defaults:
1281 if part not in defaults:
1264 if part[0] in "HMS":
1282 if part[0] in "HMS":
1265 defaults[part] = "00"
1283 defaults[part] = "00"
1266 elif part[0] in "dm":
1284 elif part[0] in "dm":
1267 defaults[part] = "1"
1285 defaults[part] = "1"
1268 else:
1286 else:
1269 defaults[part] = datestr(now, "%" + part[0], False)
1287 defaults[part] = datestr(now, "%" + part[0], False)
1270
1288
1271 for format in formats:
1289 for format in formats:
1272 try:
1290 try:
1273 when, offset = strdate(string, format, defaults)
1291 when, offset = strdate(string, format, defaults)
1274 except ValueError:
1292 except ValueError:
1275 pass
1293 pass
1276 else:
1294 else:
1277 break
1295 break
1278 else:
1296 else:
1279 raise Abort(_('invalid date: %r ') % string)
1297 raise Abort(_('invalid date: %r ') % string)
1280 # validate explicit (probably user-specified) date and
1298 # validate explicit (probably user-specified) date and
1281 # time zone offset. values must fit in signed 32 bits for
1299 # time zone offset. values must fit in signed 32 bits for
1282 # current 32-bit linux runtimes. timezones go from UTC-12
1300 # current 32-bit linux runtimes. timezones go from UTC-12
1283 # to UTC+14
1301 # to UTC+14
1284 if abs(when) > 0x7fffffff:
1302 if abs(when) > 0x7fffffff:
1285 raise Abort(_('date exceeds 32 bits: %d') % when)
1303 raise Abort(_('date exceeds 32 bits: %d') % when)
1286 if offset < -50400 or offset > 43200:
1304 if offset < -50400 or offset > 43200:
1287 raise Abort(_('impossible time zone offset: %d') % offset)
1305 raise Abort(_('impossible time zone offset: %d') % offset)
1288 return when, offset
1306 return when, offset
1289
1307
1290 def matchdate(date):
1308 def matchdate(date):
1291 """Return a function that matches a given date match specifier
1309 """Return a function that matches a given date match specifier
1292
1310
1293 Formats include:
1311 Formats include:
1294
1312
1295 '{date}' match a given date to the accuracy provided
1313 '{date}' match a given date to the accuracy provided
1296
1314
1297 '<{date}' on or before a given date
1315 '<{date}' on or before a given date
1298
1316
1299 '>{date}' on or after a given date
1317 '>{date}' on or after a given date
1300
1318
1301 """
1319 """
1302
1320
1303 def lower(date):
1321 def lower(date):
1304 return parsedate(date, extendeddateformats)[0]
1322 return parsedate(date, extendeddateformats)[0]
1305
1323
1306 def upper(date):
1324 def upper(date):
1307 d = dict(mb="12", HI="23", M="59", S="59")
1325 d = dict(mb="12", HI="23", M="59", S="59")
1308 for days in "31 30 29".split():
1326 for days in "31 30 29".split():
1309 try:
1327 try:
1310 d["d"] = days
1328 d["d"] = days
1311 return parsedate(date, extendeddateformats, d)[0]
1329 return parsedate(date, extendeddateformats, d)[0]
1312 except:
1330 except:
1313 pass
1331 pass
1314 d["d"] = "28"
1332 d["d"] = "28"
1315 return parsedate(date, extendeddateformats, d)[0]
1333 return parsedate(date, extendeddateformats, d)[0]
1316
1334
1317 if date[0] == "<":
1335 if date[0] == "<":
1318 when = upper(date[1:])
1336 when = upper(date[1:])
1319 return lambda x: x <= when
1337 return lambda x: x <= when
1320 elif date[0] == ">":
1338 elif date[0] == ">":
1321 when = lower(date[1:])
1339 when = lower(date[1:])
1322 return lambda x: x >= when
1340 return lambda x: x >= when
1323 elif date[0] == "-":
1341 elif date[0] == "-":
1324 try:
1342 try:
1325 days = int(date[1:])
1343 days = int(date[1:])
1326 except ValueError:
1344 except ValueError:
1327 raise Abort(_("invalid day spec: %s") % date[1:])
1345 raise Abort(_("invalid day spec: %s") % date[1:])
1328 when = makedate()[0] - days * 3600 * 24
1346 when = makedate()[0] - days * 3600 * 24
1329 return lambda x: x >= when
1347 return lambda x: x >= when
1330 elif " to " in date:
1348 elif " to " in date:
1331 a, b = date.split(" to ")
1349 a, b = date.split(" to ")
1332 start, stop = lower(a), upper(b)
1350 start, stop = lower(a), upper(b)
1333 return lambda x: x >= start and x <= stop
1351 return lambda x: x >= start and x <= stop
1334 else:
1352 else:
1335 start, stop = lower(date), upper(date)
1353 start, stop = lower(date), upper(date)
1336 return lambda x: x >= start and x <= stop
1354 return lambda x: x >= start and x <= stop
1337
1355
1338 def shortuser(user):
1356 def shortuser(user):
1339 """Return a short representation of a user name or email address."""
1357 """Return a short representation of a user name or email address."""
1340 f = user.find('@')
1358 f = user.find('@')
1341 if f >= 0:
1359 if f >= 0:
1342 user = user[:f]
1360 user = user[:f]
1343 f = user.find('<')
1361 f = user.find('<')
1344 if f >= 0:
1362 if f >= 0:
1345 user = user[f+1:]
1363 user = user[f+1:]
1346 f = user.find(' ')
1364 f = user.find(' ')
1347 if f >= 0:
1365 if f >= 0:
1348 user = user[:f]
1366 user = user[:f]
1349 f = user.find('.')
1367 f = user.find('.')
1350 if f >= 0:
1368 if f >= 0:
1351 user = user[:f]
1369 user = user[:f]
1352 return user
1370 return user
1353
1371
1354 def ellipsis(text, maxlength=400):
1372 def ellipsis(text, maxlength=400):
1355 """Trim string to at most maxlength (default: 400) characters."""
1373 """Trim string to at most maxlength (default: 400) characters."""
1356 if len(text) <= maxlength:
1374 if len(text) <= maxlength:
1357 return text
1375 return text
1358 else:
1376 else:
1359 return "%s..." % (text[:maxlength-3])
1377 return "%s..." % (text[:maxlength-3])
1360
1378
1361 def walkrepos(path):
1379 def walkrepos(path):
1362 '''yield every hg repository under path, recursively.'''
1380 '''yield every hg repository under path, recursively.'''
1363 def errhandler(err):
1381 def errhandler(err):
1364 if err.filename == path:
1382 if err.filename == path:
1365 raise err
1383 raise err
1366
1384
1367 for root, dirs, files in os.walk(path, onerror=errhandler):
1385 for root, dirs, files in os.walk(path, onerror=errhandler):
1368 for d in dirs:
1386 for d in dirs:
1369 if d == '.hg':
1387 if d == '.hg':
1370 yield root
1388 yield root
1371 dirs[:] = []
1389 dirs[:] = []
1372 break
1390 break
1373
1391
1374 _rcpath = None
1392 _rcpath = None
1375
1393
1376 def rcpath():
1394 def rcpath():
1377 '''return hgrc search path. if env var HGRCPATH is set, use it.
1395 '''return hgrc search path. if env var HGRCPATH is set, use it.
1378 for each item in path, if directory, use files ending in .rc,
1396 for each item in path, if directory, use files ending in .rc,
1379 else use item.
1397 else use item.
1380 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1398 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1381 if no HGRCPATH, use default os-specific path.'''
1399 if no HGRCPATH, use default os-specific path.'''
1382 global _rcpath
1400 global _rcpath
1383 if _rcpath is None:
1401 if _rcpath is None:
1384 if 'HGRCPATH' in os.environ:
1402 if 'HGRCPATH' in os.environ:
1385 _rcpath = []
1403 _rcpath = []
1386 for p in os.environ['HGRCPATH'].split(os.pathsep):
1404 for p in os.environ['HGRCPATH'].split(os.pathsep):
1387 if not p: continue
1405 if not p: continue
1388 if os.path.isdir(p):
1406 if os.path.isdir(p):
1389 for f in os.listdir(p):
1407 for f in os.listdir(p):
1390 if f.endswith('.rc'):
1408 if f.endswith('.rc'):
1391 _rcpath.append(os.path.join(p, f))
1409 _rcpath.append(os.path.join(p, f))
1392 else:
1410 else:
1393 _rcpath.append(p)
1411 _rcpath.append(p)
1394 else:
1412 else:
1395 _rcpath = os_rcpath()
1413 _rcpath = os_rcpath()
1396 return _rcpath
1414 return _rcpath
1397
1415
1398 def bytecount(nbytes):
1416 def bytecount(nbytes):
1399 '''return byte count formatted as readable string, with units'''
1417 '''return byte count formatted as readable string, with units'''
1400
1418
1401 units = (
1419 units = (
1402 (100, 1<<30, _('%.0f GB')),
1420 (100, 1<<30, _('%.0f GB')),
1403 (10, 1<<30, _('%.1f GB')),
1421 (10, 1<<30, _('%.1f GB')),
1404 (1, 1<<30, _('%.2f GB')),
1422 (1, 1<<30, _('%.2f GB')),
1405 (100, 1<<20, _('%.0f MB')),
1423 (100, 1<<20, _('%.0f MB')),
1406 (10, 1<<20, _('%.1f MB')),
1424 (10, 1<<20, _('%.1f MB')),
1407 (1, 1<<20, _('%.2f MB')),
1425 (1, 1<<20, _('%.2f MB')),
1408 (100, 1<<10, _('%.0f KB')),
1426 (100, 1<<10, _('%.0f KB')),
1409 (10, 1<<10, _('%.1f KB')),
1427 (10, 1<<10, _('%.1f KB')),
1410 (1, 1<<10, _('%.2f KB')),
1428 (1, 1<<10, _('%.2f KB')),
1411 (1, 1, _('%.0f bytes')),
1429 (1, 1, _('%.0f bytes')),
1412 )
1430 )
1413
1431
1414 for multiplier, divisor, format in units:
1432 for multiplier, divisor, format in units:
1415 if nbytes >= divisor * multiplier:
1433 if nbytes >= divisor * multiplier:
1416 return format % (nbytes / float(divisor))
1434 return format % (nbytes / float(divisor))
1417 return units[-1][2] % nbytes
1435 return units[-1][2] % nbytes
1418
1436
1419 def drop_scheme(scheme, path):
1437 def drop_scheme(scheme, path):
1420 sc = scheme + ':'
1438 sc = scheme + ':'
1421 if path.startswith(sc):
1439 if path.startswith(sc):
1422 path = path[len(sc):]
1440 path = path[len(sc):]
1423 if path.startswith('//'):
1441 if path.startswith('//'):
1424 path = path[2:]
1442 path = path[2:]
1425 return path
1443 return path
@@ -1,137 +1,142 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 hg init a
3 hg init a
4 cd a
4 cd a
5
5
6 echo start > start
6 echo start > start
7 hg ci -Amstart -d '0 0'
7 hg ci -Amstart -d '0 0'
8 echo new > new
8 echo new > new
9 hg ci -Amnew -d '0 0'
9 hg ci -Amnew -d '0 0'
10 echo '% new file'
10 echo '% new file'
11 hg diff --git -r 0
11 hg diff --git -r 0
12
12
13 hg cp new copy
13 hg cp new copy
14 hg ci -mcopy -d '0 0'
14 hg ci -mcopy -d '0 0'
15 echo '% copy'
15 echo '% copy'
16 hg diff --git -r 1:tip
16 hg diff --git -r 1:tip
17
17
18 hg mv copy rename
18 hg mv copy rename
19 hg ci -mrename -d '0 0'
19 hg ci -mrename -d '0 0'
20 echo '% rename'
20 echo '% rename'
21 hg diff --git -r 2:tip
21 hg diff --git -r 2:tip
22
22
23 hg rm rename
23 hg rm rename
24 hg ci -mdelete -d '0 0'
24 hg ci -mdelete -d '0 0'
25 echo '% delete'
25 echo '% delete'
26 hg diff --git -r 3:tip
26 hg diff --git -r 3:tip
27
27
28 cat > src <<EOF
28 cat > src <<EOF
29 1
29 1
30 2
30 2
31 3
31 3
32 4
32 4
33 5
33 5
34 EOF
34 EOF
35 hg ci -Amsrc -d '0 0'
35 hg ci -Amsrc -d '0 0'
36 chmod +x src
36 chmod +x src
37 hg ci -munexec -d '0 0'
37 hg ci -munexec -d '0 0'
38 echo '% chmod 644'
38 echo '% chmod 644'
39 hg diff --git -r 5:tip
39 hg diff --git -r 5:tip
40
40
41 hg mv src dst
41 hg mv src dst
42 chmod -x dst
42 chmod -x dst
43 echo a >> dst
43 echo a >> dst
44 hg ci -mrenamemod -d '0 0'
44 hg ci -mrenamemod -d '0 0'
45 echo '% rename+mod+chmod'
45 echo '% rename+mod+chmod'
46 hg diff --git -r 6:tip
46 hg diff --git -r 6:tip
47
47
48 echo '% nonexistent in tip+chmod'
48 echo '% nonexistent in tip+chmod'
49 hg diff --git -r 5:6
49 hg diff --git -r 5:6
50
50
51 echo '% binary diff'
51 echo '% binary diff'
52 cp $TESTDIR/binfile.bin .
52 cp $TESTDIR/binfile.bin .
53 hg add binfile.bin
53 hg add binfile.bin
54 hg diff --git > b.diff
54 hg diff --git > b.diff
55 cat b.diff
55 cat b.diff
56
56
57 echo '% import binary diff'
57 echo '% import binary diff'
58 hg revert binfile.bin
58 hg revert binfile.bin
59 rm binfile.bin
59 rm binfile.bin
60 hg import -mfoo b.diff
60 hg import -mfoo b.diff
61 cmp binfile.bin $TESTDIR/binfile.bin
61 cmp binfile.bin $TESTDIR/binfile.bin
62
62
63 echo
63 echo
64 echo '% rename binary file'
65 hg mv binfile.bin renamed.bin
66 hg diff --git
67
68 echo
64 echo '% diff across many revisions'
69 echo '% diff across many revisions'
65 hg mv dst dst2
70 hg mv dst dst2
66 hg ci -m 'mv dst dst2' -d '0 0'
71 hg ci -m 'mv dst dst2' -d '0 0'
67
72
68 echo >> start
73 echo >> start
69 hg ci -m 'change start' -d '0 0'
74 hg ci -m 'change start' -d '0 0'
70
75
71 hg revert -r -2 start
76 hg revert -r -2 start
72 hg mv dst2 dst3
77 hg mv dst2 dst3
73 hg ci -m 'mv dst2 dst3; revert start' -d '0 0'
78 hg ci -m 'mv dst2 dst3; revert start' -d '0 0'
74
79
75 hg diff --git -r 9:11
80 hg diff --git -r 9:11
76
81
77 echo a >> foo
82 echo a >> foo
78 hg add foo
83 hg add foo
79 hg ci -m 'add foo'
84 hg ci -m 'add foo'
80 echo b >> foo
85 echo b >> foo
81 hg ci -m 'change foo'
86 hg ci -m 'change foo'
82 hg mv foo bar
87 hg mv foo bar
83 hg ci -m 'mv foo bar'
88 hg ci -m 'mv foo bar'
84 echo c >> bar
89 echo c >> bar
85 hg ci -m 'change bar'
90 hg ci -m 'change bar'
86
91
87 echo
92 echo
88 echo '% file created before r1 and renamed before r2'
93 echo '% file created before r1 and renamed before r2'
89 hg diff --git -r -3:-1
94 hg diff --git -r -3:-1
90 echo
95 echo
91 echo '% file created in r1 and renamed before r2'
96 echo '% file created in r1 and renamed before r2'
92 hg diff --git -r -4:-1
97 hg diff --git -r -4:-1
93 echo
98 echo
94 echo '% file created after r1 and renamed before r2'
99 echo '% file created after r1 and renamed before r2'
95 hg diff --git -r -5:-1
100 hg diff --git -r -5:-1
96
101
97 echo
102 echo
98 echo '% comparing with the working dir'
103 echo '% comparing with the working dir'
99 echo >> start
104 echo >> start
100 hg ci -m 'change start again' -d '0 0'
105 hg ci -m 'change start again' -d '0 0'
101
106
102 echo > created
107 echo > created
103 hg add created
108 hg add created
104 hg ci -m 'add created'
109 hg ci -m 'add created'
105
110
106 hg mv created created2
111 hg mv created created2
107 hg ci -m 'mv created created2'
112 hg ci -m 'mv created created2'
108
113
109 hg mv created2 created3
114 hg mv created2 created3
110 echo "% there's a copy in the working dir..."
115 echo "% there's a copy in the working dir..."
111 hg diff --git
116 hg diff --git
112 echo
117 echo
113 echo "% ...but there's another copy between the original rev and the wd"
118 echo "% ...but there's another copy between the original rev and the wd"
114 hg diff --git -r -2
119 hg diff --git -r -2
115 echo
120 echo
116 echo "% ...but the source of the copy was created after the original rev"
121 echo "% ...but the source of the copy was created after the original rev"
117 hg diff --git -r -3
122 hg diff --git -r -3
118 hg ci -m 'mv created2 created3'
123 hg ci -m 'mv created2 created3'
119
124
120 echo > brand-new
125 echo > brand-new
121 hg add brand-new
126 hg add brand-new
122 hg ci -m 'add brand-new'
127 hg ci -m 'add brand-new'
123 hg mv brand-new brand-new2
128 hg mv brand-new brand-new2
124 echo '% created in parent of wd; renamed in the wd'
129 echo '% created in parent of wd; renamed in the wd'
125 hg diff --git
130 hg diff --git
126
131
127 echo
132 echo
128 echo '% created between r1 and parent of wd; renamed in the wd'
133 echo '% created between r1 and parent of wd; renamed in the wd'
129 hg diff --git -r -2
134 hg diff --git -r -2
130 hg ci -m 'mv brand-new brand-new2'
135 hg ci -m 'mv brand-new brand-new2'
131
136
132 echo '% one file is copied to many destinations and removed'
137 echo '% one file is copied to many destinations and removed'
133 hg cp brand-new2 brand-new3
138 hg cp brand-new2 brand-new3
134 hg mv brand-new2 brand-new3-2
139 hg mv brand-new2 brand-new3-2
135 hg ci -m 'multiple renames/copies'
140 hg ci -m 'multiple renames/copies'
136 hg diff --git -r -2 -r -1
141 hg diff --git -r -2 -r -1
137
142
@@ -1,142 +1,147 b''
1 adding start
1 adding start
2 adding new
2 adding new
3 % new file
3 % new file
4 diff --git a/new b/new
4 diff --git a/new b/new
5 new file mode 100644
5 new file mode 100644
6 --- /dev/null
6 --- /dev/null
7 +++ b/new
7 +++ b/new
8 @@ -0,0 +1,1 @@
8 @@ -0,0 +1,1 @@
9 +new
9 +new
10 % copy
10 % copy
11 diff --git a/new b/copy
11 diff --git a/new b/copy
12 copy from new
12 copy from new
13 copy to copy
13 copy to copy
14 % rename
14 % rename
15 diff --git a/copy b/rename
15 diff --git a/copy b/rename
16 rename from copy
16 rename from copy
17 rename to rename
17 rename to rename
18 % delete
18 % delete
19 diff --git a/rename b/rename
19 diff --git a/rename b/rename
20 deleted file mode 100644
20 deleted file mode 100644
21 --- a/rename
21 --- a/rename
22 +++ /dev/null
22 +++ /dev/null
23 @@ -1,1 +0,0 @@
23 @@ -1,1 +0,0 @@
24 -new
24 -new
25 adding src
25 adding src
26 % chmod 644
26 % chmod 644
27 diff --git a/src b/src
27 diff --git a/src b/src
28 old mode 100644
28 old mode 100644
29 new mode 100755
29 new mode 100755
30 % rename+mod+chmod
30 % rename+mod+chmod
31 diff --git a/src b/dst
31 diff --git a/src b/dst
32 old mode 100755
32 old mode 100755
33 new mode 100644
33 new mode 100644
34 rename from src
34 rename from src
35 rename to dst
35 rename to dst
36 --- a/dst
36 --- a/dst
37 +++ b/dst
37 +++ b/dst
38 @@ -3,3 +3,4 @@ 3
38 @@ -3,3 +3,4 @@ 3
39 3
39 3
40 4
40 4
41 5
41 5
42 +a
42 +a
43 % nonexistent in tip+chmod
43 % nonexistent in tip+chmod
44 diff --git a/src b/src
44 diff --git a/src b/src
45 old mode 100644
45 old mode 100644
46 new mode 100755
46 new mode 100755
47 % binary diff
47 % binary diff
48 diff --git a/binfile.bin b/binfile.bin
48 diff --git a/binfile.bin b/binfile.bin
49 new file mode 100644
49 new file mode 100644
50 index 0000000000000000000000000000000000000000..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
50 index 0000000000000000000000000000000000000000..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
51 GIT binary patch
51 GIT binary patch
52 literal 593
52 literal 593
53 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
53 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
54 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
54 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
55 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
55 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
56 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
56 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
57 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
57 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
58 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
58 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
59 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
59 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
60 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
60 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
61 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
61 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
62 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
62 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
63 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
63 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
64 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
64 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
65
65
66 % import binary diff
66 % import binary diff
67 applying b.diff
67 applying b.diff
68
68
69 % rename binary file
70 diff --git a/binfile.bin b/renamed.bin
71 rename from binfile.bin
72 rename to renamed.bin
73
69 % diff across many revisions
74 % diff across many revisions
70 diff --git a/dst2 b/dst3
75 diff --git a/dst2 b/dst3
71 rename from dst2
76 rename from dst2
72 rename to dst3
77 rename to dst3
73
78
74 % file created before r1 and renamed before r2
79 % file created before r1 and renamed before r2
75 diff --git a/foo b/bar
80 diff --git a/foo b/bar
76 rename from foo
81 rename from foo
77 rename to bar
82 rename to bar
78 --- a/bar
83 --- a/bar
79 +++ b/bar
84 +++ b/bar
80 @@ -1,2 +1,3 @@ a
85 @@ -1,2 +1,3 @@ a
81 a
86 a
82 b
87 b
83 +c
88 +c
84
89
85 % file created in r1 and renamed before r2
90 % file created in r1 and renamed before r2
86 diff --git a/foo b/bar
91 diff --git a/foo b/bar
87 rename from foo
92 rename from foo
88 rename to bar
93 rename to bar
89 --- a/bar
94 --- a/bar
90 +++ b/bar
95 +++ b/bar
91 @@ -1,1 +1,3 @@ a
96 @@ -1,1 +1,3 @@ a
92 a
97 a
93 +b
98 +b
94 +c
99 +c
95
100
96 % file created after r1 and renamed before r2
101 % file created after r1 and renamed before r2
97 diff --git a/bar b/bar
102 diff --git a/bar b/bar
98 new file mode 100644
103 new file mode 100644
99 --- /dev/null
104 --- /dev/null
100 +++ b/bar
105 +++ b/bar
101 @@ -0,0 +1,3 @@
106 @@ -0,0 +1,3 @@
102 +a
107 +a
103 +b
108 +b
104 +c
109 +c
105
110
106 % comparing with the working dir
111 % comparing with the working dir
107 % there's a copy in the working dir...
112 % there's a copy in the working dir...
108 diff --git a/created2 b/created3
113 diff --git a/created2 b/created3
109 rename from created2
114 rename from created2
110 rename to created3
115 rename to created3
111
116
112 % ...but there's another copy between the original rev and the wd
117 % ...but there's another copy between the original rev and the wd
113 diff --git a/created b/created3
118 diff --git a/created b/created3
114 rename from created
119 rename from created
115 rename to created3
120 rename to created3
116
121
117 % ...but the source of the copy was created after the original rev
122 % ...but the source of the copy was created after the original rev
118 diff --git a/created3 b/created3
123 diff --git a/created3 b/created3
119 new file mode 100644
124 new file mode 100644
120 --- /dev/null
125 --- /dev/null
121 +++ b/created3
126 +++ b/created3
122 @@ -0,0 +1,1 @@
127 @@ -0,0 +1,1 @@
123 +
128 +
124 % created in parent of wd; renamed in the wd
129 % created in parent of wd; renamed in the wd
125 diff --git a/brand-new b/brand-new2
130 diff --git a/brand-new b/brand-new2
126 rename from brand-new
131 rename from brand-new
127 rename to brand-new2
132 rename to brand-new2
128
133
129 % created between r1 and parent of wd; renamed in the wd
134 % created between r1 and parent of wd; renamed in the wd
130 diff --git a/brand-new2 b/brand-new2
135 diff --git a/brand-new2 b/brand-new2
131 new file mode 100644
136 new file mode 100644
132 --- /dev/null
137 --- /dev/null
133 +++ b/brand-new2
138 +++ b/brand-new2
134 @@ -0,0 +1,1 @@
139 @@ -0,0 +1,1 @@
135 +
140 +
136 % one file is copied to many destinations and removed
141 % one file is copied to many destinations and removed
137 diff --git a/brand-new2 b/brand-new3
142 diff --git a/brand-new2 b/brand-new3
138 rename from brand-new2
143 rename from brand-new2
139 rename to brand-new3
144 rename to brand-new3
140 diff --git a/brand-new2 b/brand-new3-2
145 diff --git a/brand-new2 b/brand-new3-2
141 copy from brand-new2
146 copy from brand-new2
142 copy to brand-new3-2
147 copy to brand-new3-2
@@ -1,301 +1,339 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 echo "[extensions]" >> $HGRCPATH
3 echo "[extensions]" >> $HGRCPATH
4 echo "mq=" >> $HGRCPATH
4 echo "mq=" >> $HGRCPATH
5
5
6 echo % help
6 echo % help
7 hg help mq
7 hg help mq
8
8
9 hg init a
9 hg init a
10 cd a
10 cd a
11 echo a > a
11 echo a > a
12 hg ci -Ama
12 hg ci -Ama
13
13
14 hg clone . ../k
14 hg clone . ../k
15
15
16 mkdir b
16 mkdir b
17 echo z > b/z
17 echo z > b/z
18 hg ci -Ama
18 hg ci -Ama
19
19
20 echo % qinit
20 echo % qinit
21
21
22 hg qinit
22 hg qinit
23
23
24 cd ..
24 cd ..
25 hg init b
25 hg init b
26
26
27 echo % -R qinit
27 echo % -R qinit
28
28
29 hg -R b qinit
29 hg -R b qinit
30
30
31 hg init c
31 hg init c
32
32
33 echo % qinit -c
33 echo % qinit -c
34
34
35 hg --cwd c qinit -c
35 hg --cwd c qinit -c
36 hg -R c/.hg/patches st
36 hg -R c/.hg/patches st
37
37
38 echo % qnew implies add
38 echo % qnew implies add
39
39
40 hg -R c qnew test.patch
40 hg -R c qnew test.patch
41 hg -R c/.hg/patches st
41 hg -R c/.hg/patches st
42
42
43 echo '% qinit; qinit -c'
43 echo '% qinit; qinit -c'
44 hg init d
44 hg init d
45 cd d
45 cd d
46 hg qinit
46 hg qinit
47 hg qinit -c
47 hg qinit -c
48 # qinit -c should create both files if they don't exist
48 # qinit -c should create both files if they don't exist
49 echo ' .hgignore:'
49 echo ' .hgignore:'
50 cat .hg/patches/.hgignore
50 cat .hg/patches/.hgignore
51 echo ' series:'
51 echo ' series:'
52 cat .hg/patches/series
52 cat .hg/patches/series
53 hg qinit -c 2>&1 | sed -e 's/repository.*already/repository already/'
53 hg qinit -c 2>&1 | sed -e 's/repository.*already/repository already/'
54 cd ..
54 cd ..
55
55
56 echo '% qinit; <stuff>; qinit -c'
56 echo '% qinit; <stuff>; qinit -c'
57 hg init e
57 hg init e
58 cd e
58 cd e
59 hg qnew A
59 hg qnew A
60 echo foo > foo
60 echo foo > foo
61 hg add foo
61 hg add foo
62 hg qrefresh
62 hg qrefresh
63 hg qnew B
63 hg qnew B
64 echo >> foo
64 echo >> foo
65 hg qrefresh
65 hg qrefresh
66 echo status >> .hg/patches/.hgignore
66 echo status >> .hg/patches/.hgignore
67 echo bleh >> .hg/patches/.hgignore
67 echo bleh >> .hg/patches/.hgignore
68 hg qinit -c
68 hg qinit -c
69 hg -R .hg/patches status
69 hg -R .hg/patches status
70 # qinit -c shouldn't touch these files if they already exist
70 # qinit -c shouldn't touch these files if they already exist
71 echo ' .hgignore:'
71 echo ' .hgignore:'
72 cat .hg/patches/.hgignore
72 cat .hg/patches/.hgignore
73 echo ' series:'
73 echo ' series:'
74 cat .hg/patches/series
74 cat .hg/patches/series
75 cd ..
75 cd ..
76
76
77 cd a
77 cd a
78
78
79 echo % qnew -m
79 echo % qnew -m
80
80
81 hg qnew -m 'foo bar' test.patch
81 hg qnew -m 'foo bar' test.patch
82 cat .hg/patches/test.patch
82 cat .hg/patches/test.patch
83
83
84 echo % qrefresh
84 echo % qrefresh
85
85
86 echo a >> a
86 echo a >> a
87 hg qrefresh
87 hg qrefresh
88 sed -e "s/^\(diff -r \)\([a-f0-9]* \)/\1 x/" \
88 sed -e "s/^\(diff -r \)\([a-f0-9]* \)/\1 x/" \
89 -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
89 -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
90 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/test.patch
90 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/test.patch
91
91
92 echo % qpop
92 echo % qpop
93
93
94 hg qpop
94 hg qpop
95
95
96 echo % qpush
96 echo % qpush
97
97
98 hg qpush
98 hg qpush
99
99
100 cd ..
100 cd ..
101
101
102 echo % pop/push outside repo
102 echo % pop/push outside repo
103
103
104 hg -R a qpop
104 hg -R a qpop
105 hg -R a qpush
105 hg -R a qpush
106
106
107 cd a
107 cd a
108 hg qnew test2.patch
108 hg qnew test2.patch
109
109
110 echo % qrefresh in subdir
110 echo % qrefresh in subdir
111
111
112 cd b
112 cd b
113 echo a > a
113 echo a > a
114 hg add a
114 hg add a
115 hg qrefresh
115 hg qrefresh
116
116
117 echo % pop/push -a in subdir
117 echo % pop/push -a in subdir
118
118
119 hg qpop -a
119 hg qpop -a
120 hg --traceback qpush -a
120 hg --traceback qpush -a
121
121
122 echo % qseries
122 echo % qseries
123 hg qseries
123 hg qseries
124 hg qpop
124 hg qpop
125 hg qseries -vs
125 hg qseries -vs
126 hg qpush
126 hg qpush
127
127
128 echo % qapplied
128 echo % qapplied
129 hg qapplied
129 hg qapplied
130
130
131 echo % qtop
131 echo % qtop
132 hg qtop
132 hg qtop
133
133
134 echo % qprev
134 echo % qprev
135 hg qprev
135 hg qprev
136
136
137 echo % qnext
137 echo % qnext
138 hg qnext
138 hg qnext
139
139
140 echo % pop, qnext, qprev, qapplied
140 echo % pop, qnext, qprev, qapplied
141 hg qpop
141 hg qpop
142 hg qnext
142 hg qnext
143 hg qprev
143 hg qprev
144 hg qapplied
144 hg qapplied
145
145
146 echo % commit should fail
146 echo % commit should fail
147 hg commit
147 hg commit
148
148
149 echo % push should fail
149 echo % push should fail
150 hg push ../../k
150 hg push ../../k
151
151
152 echo % qunapplied
152 echo % qunapplied
153 hg qunapplied
153 hg qunapplied
154
154
155 echo % qpush/qpop with index
155 echo % qpush/qpop with index
156 hg qnew test1b.patch
156 hg qnew test1b.patch
157 echo 1b > 1b
157 echo 1b > 1b
158 hg add 1b
158 hg add 1b
159 hg qrefresh
159 hg qrefresh
160 hg qpush 2
160 hg qpush 2
161 hg qpop 0
161 hg qpop 0
162 hg qpush test.patch+1
162 hg qpush test.patch+1
163 hg qpush test.patch+2
163 hg qpush test.patch+2
164 hg qpop test2.patch-1
164 hg qpop test2.patch-1
165 hg qpop test2.patch-2
165 hg qpop test2.patch-2
166 hg qpush test1b.patch+1
166 hg qpush test1b.patch+1
167
167
168 echo % push should succeed
168 echo % push should succeed
169 hg qpop -a
169 hg qpop -a
170 hg push ../../k
170 hg push ../../k
171
171
172 echo % strip
172 echo % strip
173 cd ../../b
173 cd ../../b
174 echo x>x
174 echo x>x
175 hg ci -Ama
175 hg ci -Ama
176 hg strip tip 2>&1 | sed 's/\(saving bundle to \).*/\1/'
176 hg strip tip 2>&1 | sed 's/\(saving bundle to \).*/\1/'
177 hg unbundle .hg/strip-backup/*
177 hg unbundle .hg/strip-backup/*
178
178
179 echo '% cd b; hg qrefresh'
179 echo '% cd b; hg qrefresh'
180 hg init refresh
180 hg init refresh
181 cd refresh
181 cd refresh
182 echo a > a
182 echo a > a
183 hg ci -Ama -d'0 0'
183 hg ci -Ama -d'0 0'
184 hg qnew -mfoo foo
184 hg qnew -mfoo foo
185 echo a >> a
185 echo a >> a
186 hg qrefresh
186 hg qrefresh
187 mkdir b
187 mkdir b
188 cd b
188 cd b
189 echo f > f
189 echo f > f
190 hg add f
190 hg add f
191 hg qrefresh
191 hg qrefresh
192 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
192 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
193 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" ../.hg/patches/foo
193 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" ../.hg/patches/foo
194 echo % hg qrefresh .
194 echo % hg qrefresh .
195 hg qrefresh .
195 hg qrefresh .
196 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
196 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
197 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" ../.hg/patches/foo
197 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" ../.hg/patches/foo
198 hg status
198 hg status
199
199
200 echo % qpush failure
200 echo % qpush failure
201 cd ..
201 cd ..
202 hg qrefresh
202 hg qrefresh
203 hg qnew -mbar bar
203 hg qnew -mbar bar
204 echo foo > foo
204 echo foo > foo
205 echo bar > bar
205 echo bar > bar
206 hg add foo bar
206 hg add foo bar
207 hg qrefresh
207 hg qrefresh
208 hg qpop -a
208 hg qpop -a
209 echo bar > foo
209 echo bar > foo
210 hg qpush -a
210 hg qpush -a
211 hg st
211 hg st
212
212
213 cat >>$HGRCPATH <<EOF
213 cat >>$HGRCPATH <<EOF
214 [diff]
214 [diff]
215 git = True
215 git = True
216 EOF
216 EOF
217 cd ..
217 cd ..
218 hg init git
218 hg init git
219 cd git
219 cd git
220 hg qinit
220 hg qinit
221
221
222 hg qnew -m'new file' new
222 hg qnew -m'new file' new
223 echo foo > new
223 echo foo > new
224 chmod +x new
224 chmod +x new
225 hg add new
225 hg add new
226 hg qrefresh
226 hg qrefresh
227 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
227 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
228 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/new
228 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/new
229
229
230 hg qnew -m'copy file' copy
230 hg qnew -m'copy file' copy
231 hg cp new copy
231 hg cp new copy
232 hg qrefresh
232 hg qrefresh
233 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
233 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
234 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/copy
234 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/copy
235
235
236 hg qpop
236 hg qpop
237 hg qpush
237 hg qpush
238 hg qdiff
238 hg qdiff
239 cat >>$HGRCPATH <<EOF
239 cat >>$HGRCPATH <<EOF
240 [diff]
240 [diff]
241 git = False
241 git = False
242 EOF
242 EOF
243 hg qdiff --git
243 hg qdiff --git
244
244
245 cd ..
245 cd ..
246 hg init slow
246 hg init slow
247 cd slow
247 cd slow
248 hg qinit
248 hg qinit
249 echo foo > foo
249 echo foo > foo
250 hg add foo
250 hg add foo
251 hg ci -m 'add foo'
251 hg ci -m 'add foo'
252 hg qnew bar
252 hg qnew bar
253 echo bar > bar
253 echo bar > bar
254 hg add bar
254 hg add bar
255 hg mv foo baz
255 hg mv foo baz
256 hg qrefresh --git
256 hg qrefresh --git
257 hg up -C 0
257 hg up -C 0
258 echo >> foo
258 echo >> foo
259 hg ci -m 'change foo'
259 hg ci -m 'change foo'
260 hg up -C 1
260 hg up -C 1
261 hg qrefresh --git 2>&1 | grep -v 'saving bundle'
261 hg qrefresh --git 2>&1 | grep -v 'saving bundle'
262 cat .hg/patches/bar
262 cat .hg/patches/bar
263 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
263 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
264 hg qrefresh --git
264 hg qrefresh --git
265 cat .hg/patches/bar
265 cat .hg/patches/bar
266 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
266 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
267
267
268 echo
268 echo
269 hg up -C 1
269 hg up -C 1
270 echo >> foo
270 echo >> foo
271 hg ci -m 'change foo again'
271 hg ci -m 'change foo again'
272 hg up -C 2
272 hg up -C 2
273 hg mv bar quux
273 hg mv bar quux
274 hg mv baz bleh
274 hg mv baz bleh
275 hg qrefresh --git 2>&1 | grep -v 'saving bundle'
275 hg qrefresh --git 2>&1 | grep -v 'saving bundle'
276 cat .hg/patches/bar
276 cat .hg/patches/bar
277 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
277 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
278 hg mv quux fred
278 hg mv quux fred
279 hg mv bleh barney
279 hg mv bleh barney
280 hg qrefresh --git
280 hg qrefresh --git
281 cat .hg/patches/bar
281 cat .hg/patches/bar
282 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
282 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
283
283
284 echo '% strip again'
284 echo '% strip again'
285 cd ..
285 cd ..
286 hg init strip
286 hg init strip
287 cd strip
287 cd strip
288 touch foo
288 touch foo
289 hg add foo
289 hg add foo
290 hg ci -m 'add foo' -d '0 0'
290 hg ci -m 'add foo' -d '0 0'
291 echo >> foo
291 echo >> foo
292 hg ci -m 'change foo 1' -d '0 0'
292 hg ci -m 'change foo 1' -d '0 0'
293 hg up -C 0
293 hg up -C 0
294 echo 1 >> foo
294 echo 1 >> foo
295 hg ci -m 'change foo 2' -d '0 0'
295 hg ci -m 'change foo 2' -d '0 0'
296 HGMERGE=true hg merge
296 HGMERGE=true hg merge
297 hg ci -m merge -d '0 0'
297 hg ci -m merge -d '0 0'
298 hg log
298 hg log
299 hg strip 1 2>&1 | sed 's/\(saving bundle to \).*/\1/'
299 hg strip 1 2>&1 | sed 's/\(saving bundle to \).*/\1/'
300 hg log
300 hg log
301 cd ..
301
302
303 echo '% qclone'
304 qlog()
305 {
306 echo 'main repo:'
307 hg log --template ' rev {rev}: {desc}\n'
308 echo 'patch repo:'
309 hg -R .hg/patches log --template ' rev {rev}: {desc}\n'
310 }
311 hg init qclonesource
312 cd qclonesource
313 echo foo > foo
314 hg add foo
315 hg ci -m 'add foo'
316 hg qinit -c
317 hg qnew patch1
318 echo bar >> foo
319 hg qrefresh -m 'change foo'
320 hg qci -m checkpoint
321 qlog
322 cd ..
323
324 # repo with patches applied
325 hg qclone qclonesource qclonedest
326 cd qclonedest
327 qlog
328 cd ..
329
330 # repo with patches unapplied
331 cd qclonesource
332 hg qpop -a
333 qlog
334 cd ..
335 hg qclone qclonesource qclonedest2
336 cd qclonedest2
337 qlog
338 cd ..
339
@@ -1,335 +1,358 b''
1 % help
1 % help
2 mq extension - patch management and development
2 mq extension - patch management and development
3
3
4 This extension lets you work with a stack of patches in a Mercurial
4 This extension lets you work with a stack of patches in a Mercurial
5 repository. It manages two stacks of patches - all known patches, and
5 repository. It manages two stacks of patches - all known patches, and
6 applied patches (subset of known patches).
6 applied patches (subset of known patches).
7
7
8 Known patches are represented as patch files in the .hg/patches
8 Known patches are represented as patch files in the .hg/patches
9 directory. Applied patches are both patch files and changesets.
9 directory. Applied patches are both patch files and changesets.
10
10
11 Common tasks (use "hg help command" for more details):
11 Common tasks (use "hg help command" for more details):
12
12
13 prepare repository to work with patches qinit
13 prepare repository to work with patches qinit
14 create new patch qnew
14 create new patch qnew
15 import existing patch qimport
15 import existing patch qimport
16
16
17 print patch series qseries
17 print patch series qseries
18 print applied patches qapplied
18 print applied patches qapplied
19 print name of top applied patch qtop
19 print name of top applied patch qtop
20
20
21 add known patch to applied stack qpush
21 add known patch to applied stack qpush
22 remove patch from applied stack qpop
22 remove patch from applied stack qpop
23 refresh contents of top applied patch qrefresh
23 refresh contents of top applied patch qrefresh
24
24
25 list of commands (use "hg help -v mq" to show aliases and global options):
25 list of commands (use "hg help -v mq" to show aliases and global options):
26
26
27 qapplied print the patches already applied
27 qapplied print the patches already applied
28 qclone clone main and patch repository at same time
28 qclone clone main and patch repository at same time
29 qcommit commit changes in the queue repository
29 qcommit commit changes in the queue repository
30 qdelete remove patches from queue
30 qdelete remove patches from queue
31 qdiff diff of the current patch
31 qdiff diff of the current patch
32 qfold fold the named patches into the current patch
32 qfold fold the named patches into the current patch
33 qguard set or print guards for a patch
33 qguard set or print guards for a patch
34 qheader Print the header of the topmost or specified patch
34 qheader Print the header of the topmost or specified patch
35 qimport import a patch
35 qimport import a patch
36 qinit init a new queue repository
36 qinit init a new queue repository
37 qnew create a new patch
37 qnew create a new patch
38 qnext print the name of the next patch
38 qnext print the name of the next patch
39 qpop pop the current patch off the stack
39 qpop pop the current patch off the stack
40 qprev print the name of the previous patch
40 qprev print the name of the previous patch
41 qpush push the next patch onto the stack
41 qpush push the next patch onto the stack
42 qrefresh update the current patch
42 qrefresh update the current patch
43 qrename rename a patch
43 qrename rename a patch
44 qrestore restore the queue state saved by a rev
44 qrestore restore the queue state saved by a rev
45 qsave save current queue state
45 qsave save current queue state
46 qselect set or print guarded patches to push
46 qselect set or print guarded patches to push
47 qseries print the entire series file
47 qseries print the entire series file
48 qtop print the name of the current patch
48 qtop print the name of the current patch
49 qunapplied print the patches not yet applied
49 qunapplied print the patches not yet applied
50 strip strip a revision and all later revs on the same branch
50 strip strip a revision and all later revs on the same branch
51 adding a
51 adding a
52 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
52 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 adding b/z
53 adding b/z
54 % qinit
54 % qinit
55 % -R qinit
55 % -R qinit
56 % qinit -c
56 % qinit -c
57 A .hgignore
57 A .hgignore
58 A series
58 A series
59 % qnew implies add
59 % qnew implies add
60 A .hgignore
60 A .hgignore
61 A series
61 A series
62 A test.patch
62 A test.patch
63 % qinit; qinit -c
63 % qinit; qinit -c
64 .hgignore:
64 .hgignore:
65 syntax: glob
65 syntax: glob
66 status
66 status
67 guards
67 guards
68 series:
68 series:
69 abort: repository already exists!
69 abort: repository already exists!
70 % qinit; <stuff>; qinit -c
70 % qinit; <stuff>; qinit -c
71 adding A
71 adding A
72 adding B
72 adding B
73 A .hgignore
73 A .hgignore
74 A A
74 A A
75 A B
75 A B
76 A series
76 A series
77 .hgignore:
77 .hgignore:
78 status
78 status
79 bleh
79 bleh
80 series:
80 series:
81 A
81 A
82 B
82 B
83 % qnew -m
83 % qnew -m
84 foo bar
84 foo bar
85 % qrefresh
85 % qrefresh
86 foo bar
86 foo bar
87
87
88 diff -r xa
88 diff -r xa
89 --- a/a
89 --- a/a
90 +++ b/a
90 +++ b/a
91 @@ -1,1 +1,2 @@ a
91 @@ -1,1 +1,2 @@ a
92 a
92 a
93 +a
93 +a
94 % qpop
94 % qpop
95 Patch queue now empty
95 Patch queue now empty
96 % qpush
96 % qpush
97 applying test.patch
97 applying test.patch
98 Now at: test.patch
98 Now at: test.patch
99 % pop/push outside repo
99 % pop/push outside repo
100 Patch queue now empty
100 Patch queue now empty
101 applying test.patch
101 applying test.patch
102 Now at: test.patch
102 Now at: test.patch
103 % qrefresh in subdir
103 % qrefresh in subdir
104 % pop/push -a in subdir
104 % pop/push -a in subdir
105 Patch queue now empty
105 Patch queue now empty
106 applying test.patch
106 applying test.patch
107 applying test2.patch
107 applying test2.patch
108 Now at: test2.patch
108 Now at: test2.patch
109 % qseries
109 % qseries
110 test.patch
110 test.patch
111 test2.patch
111 test2.patch
112 Now at: test.patch
112 Now at: test.patch
113 0 A test.patch: foo bar
113 0 A test.patch: foo bar
114 1 U test2.patch:
114 1 U test2.patch:
115 applying test2.patch
115 applying test2.patch
116 Now at: test2.patch
116 Now at: test2.patch
117 % qapplied
117 % qapplied
118 test.patch
118 test.patch
119 test2.patch
119 test2.patch
120 % qtop
120 % qtop
121 test2.patch
121 test2.patch
122 % qprev
122 % qprev
123 test.patch
123 test.patch
124 % qnext
124 % qnext
125 All patches applied
125 All patches applied
126 % pop, qnext, qprev, qapplied
126 % pop, qnext, qprev, qapplied
127 Now at: test.patch
127 Now at: test.patch
128 test2.patch
128 test2.patch
129 Only one patch applied
129 Only one patch applied
130 test.patch
130 test.patch
131 % commit should fail
131 % commit should fail
132 abort: cannot commit over an applied mq patch
132 abort: cannot commit over an applied mq patch
133 % push should fail
133 % push should fail
134 pushing to ../../k
134 pushing to ../../k
135 abort: source has mq patches applied
135 abort: source has mq patches applied
136 % qunapplied
136 % qunapplied
137 test2.patch
137 test2.patch
138 % qpush/qpop with index
138 % qpush/qpop with index
139 applying test2.patch
139 applying test2.patch
140 Now at: test2.patch
140 Now at: test2.patch
141 Now at: test.patch
141 Now at: test.patch
142 applying test1b.patch
142 applying test1b.patch
143 Now at: test1b.patch
143 Now at: test1b.patch
144 applying test2.patch
144 applying test2.patch
145 Now at: test2.patch
145 Now at: test2.patch
146 Now at: test1b.patch
146 Now at: test1b.patch
147 Now at: test.patch
147 Now at: test.patch
148 applying test1b.patch
148 applying test1b.patch
149 applying test2.patch
149 applying test2.patch
150 Now at: test2.patch
150 Now at: test2.patch
151 % push should succeed
151 % push should succeed
152 Patch queue now empty
152 Patch queue now empty
153 pushing to ../../k
153 pushing to ../../k
154 searching for changes
154 searching for changes
155 adding changesets
155 adding changesets
156 adding manifests
156 adding manifests
157 adding file changes
157 adding file changes
158 added 1 changesets with 1 changes to 1 files
158 added 1 changesets with 1 changes to 1 files
159 % strip
159 % strip
160 adding x
160 adding x
161 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
161 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
162 saving bundle to
162 saving bundle to
163 adding changesets
163 adding changesets
164 adding manifests
164 adding manifests
165 adding file changes
165 adding file changes
166 added 1 changesets with 1 changes to 1 files
166 added 1 changesets with 1 changes to 1 files
167 (run 'hg update' to get a working copy)
167 (run 'hg update' to get a working copy)
168 % cd b; hg qrefresh
168 % cd b; hg qrefresh
169 adding a
169 adding a
170 foo
170 foo
171
171
172 diff -r cb9a9f314b8b a
172 diff -r cb9a9f314b8b a
173 --- a/a
173 --- a/a
174 +++ b/a
174 +++ b/a
175 @@ -1,1 +1,2 @@ a
175 @@ -1,1 +1,2 @@ a
176 a
176 a
177 +a
177 +a
178 diff -r cb9a9f314b8b b/f
178 diff -r cb9a9f314b8b b/f
179 --- /dev/null
179 --- /dev/null
180 +++ b/b/f
180 +++ b/b/f
181 @@ -0,0 +1,1 @@
181 @@ -0,0 +1,1 @@
182 +f
182 +f
183 % hg qrefresh .
183 % hg qrefresh .
184 foo
184 foo
185
185
186 diff -r cb9a9f314b8b b/f
186 diff -r cb9a9f314b8b b/f
187 --- /dev/null
187 --- /dev/null
188 +++ b/b/f
188 +++ b/b/f
189 @@ -0,0 +1,1 @@
189 @@ -0,0 +1,1 @@
190 +f
190 +f
191 M a
191 M a
192 % qpush failure
192 % qpush failure
193 Patch queue now empty
193 Patch queue now empty
194 applying foo
194 applying foo
195 applying bar
195 applying bar
196 1 out of 1 hunk ignored -- saving rejects to file foo.rej
196 1 out of 1 hunk ignored -- saving rejects to file foo.rej
197 patch failed, unable to continue (try -v)
197 patch failed, unable to continue (try -v)
198 patch failed, rejects left in working dir
198 patch failed, rejects left in working dir
199 Errors during apply, please fix and refresh bar
199 Errors during apply, please fix and refresh bar
200 ? foo
200 ? foo
201 ? foo.rej
201 ? foo.rej
202 new file
202 new file
203
203
204 diff --git a/new b/new
204 diff --git a/new b/new
205 new file mode 100755
205 new file mode 100755
206 --- /dev/null
206 --- /dev/null
207 +++ b/new
207 +++ b/new
208 @@ -0,0 +1,1 @@
208 @@ -0,0 +1,1 @@
209 +foo
209 +foo
210 copy file
210 copy file
211
211
212 diff --git a/new b/copy
212 diff --git a/new b/copy
213 copy from new
213 copy from new
214 copy to copy
214 copy to copy
215 Now at: new
215 Now at: new
216 applying copy
216 applying copy
217 Now at: copy
217 Now at: copy
218 diff --git a/new b/copy
218 diff --git a/new b/copy
219 copy from new
219 copy from new
220 copy to copy
220 copy to copy
221 diff --git a/new b/copy
221 diff --git a/new b/copy
222 copy from new
222 copy from new
223 copy to copy
223 copy to copy
224 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
224 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
225 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
225 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
226 adding branch
226 adding branch
227 adding changesets
227 adding changesets
228 adding manifests
228 adding manifests
229 adding file changes
229 adding file changes
230 added 1 changesets with 1 changes to 1 files
230 added 1 changesets with 1 changes to 1 files
231 (run 'hg update' to get a working copy)
231 (run 'hg update' to get a working copy)
232 Patch queue now empty
232 Patch queue now empty
233 applying bar
233 applying bar
234 Now at: bar
234 Now at: bar
235 diff --git a/bar b/bar
235 diff --git a/bar b/bar
236 new file mode 100644
236 new file mode 100644
237 --- /dev/null
237 --- /dev/null
238 +++ b/bar
238 +++ b/bar
239 @@ -0,0 +1,1 @@
239 @@ -0,0 +1,1 @@
240 +bar
240 +bar
241 diff --git a/foo b/baz
241 diff --git a/foo b/baz
242 rename from foo
242 rename from foo
243 rename to baz
243 rename to baz
244 2 baz (foo)
244 2 baz (foo)
245 diff --git a/bar b/bar
245 diff --git a/bar b/bar
246 new file mode 100644
246 new file mode 100644
247 --- /dev/null
247 --- /dev/null
248 +++ b/bar
248 +++ b/bar
249 @@ -0,0 +1,1 @@
249 @@ -0,0 +1,1 @@
250 +bar
250 +bar
251 diff --git a/foo b/baz
251 diff --git a/foo b/baz
252 rename from foo
252 rename from foo
253 rename to baz
253 rename to baz
254 2 baz (foo)
254 2 baz (foo)
255
255
256 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
256 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
257 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
257 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
258 adding branch
258 adding branch
259 adding changesets
259 adding changesets
260 adding manifests
260 adding manifests
261 adding file changes
261 adding file changes
262 added 1 changesets with 1 changes to 1 files
262 added 1 changesets with 1 changes to 1 files
263 (run 'hg update' to get a working copy)
263 (run 'hg update' to get a working copy)
264 Patch queue now empty
264 Patch queue now empty
265 applying bar
265 applying bar
266 Now at: bar
266 Now at: bar
267 diff --git a/foo b/bleh
267 diff --git a/foo b/bleh
268 rename from foo
268 rename from foo
269 rename to bleh
269 rename to bleh
270 diff --git a/quux b/quux
270 diff --git a/quux b/quux
271 new file mode 100644
271 new file mode 100644
272 --- /dev/null
272 --- /dev/null
273 +++ b/quux
273 +++ b/quux
274 @@ -0,0 +1,1 @@
274 @@ -0,0 +1,1 @@
275 +bar
275 +bar
276 3 bleh (foo)
276 3 bleh (foo)
277 diff --git a/foo b/barney
277 diff --git a/foo b/barney
278 rename from foo
278 rename from foo
279 rename to barney
279 rename to barney
280 diff --git a/fred b/fred
280 diff --git a/fred b/fred
281 new file mode 100644
281 new file mode 100644
282 --- /dev/null
282 --- /dev/null
283 +++ b/fred
283 +++ b/fred
284 @@ -0,0 +1,1 @@
284 @@ -0,0 +1,1 @@
285 +bar
285 +bar
286 3 barney (foo)
286 3 barney (foo)
287 % strip again
287 % strip again
288 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
288 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
289 merging foo
289 merging foo
290 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
290 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
291 (branch merge, don't forget to commit)
291 (branch merge, don't forget to commit)
292 changeset: 3:99615015637b
292 changeset: 3:99615015637b
293 tag: tip
293 tag: tip
294 parent: 2:20cbbe65cff7
294 parent: 2:20cbbe65cff7
295 parent: 1:d2871fc282d4
295 parent: 1:d2871fc282d4
296 user: test
296 user: test
297 date: Thu Jan 01 00:00:00 1970 +0000
297 date: Thu Jan 01 00:00:00 1970 +0000
298 summary: merge
298 summary: merge
299
299
300 changeset: 2:20cbbe65cff7
300 changeset: 2:20cbbe65cff7
301 parent: 0:53245c60e682
301 parent: 0:53245c60e682
302 user: test
302 user: test
303 date: Thu Jan 01 00:00:00 1970 +0000
303 date: Thu Jan 01 00:00:00 1970 +0000
304 summary: change foo 2
304 summary: change foo 2
305
305
306 changeset: 1:d2871fc282d4
306 changeset: 1:d2871fc282d4
307 user: test
307 user: test
308 date: Thu Jan 01 00:00:00 1970 +0000
308 date: Thu Jan 01 00:00:00 1970 +0000
309 summary: change foo 1
309 summary: change foo 1
310
310
311 changeset: 0:53245c60e682
311 changeset: 0:53245c60e682
312 user: test
312 user: test
313 date: Thu Jan 01 00:00:00 1970 +0000
313 date: Thu Jan 01 00:00:00 1970 +0000
314 summary: add foo
314 summary: add foo
315
315
316 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
316 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
317 saving bundle to
317 saving bundle to
318 saving bundle to
318 saving bundle to
319 adding branch
319 adding branch
320 adding changesets
320 adding changesets
321 adding manifests
321 adding manifests
322 adding file changes
322 adding file changes
323 added 1 changesets with 1 changes to 1 files
323 added 1 changesets with 1 changes to 1 files
324 (run 'hg update' to get a working copy)
324 (run 'hg update' to get a working copy)
325 changeset: 1:20cbbe65cff7
325 changeset: 1:20cbbe65cff7
326 tag: tip
326 tag: tip
327 user: test
327 user: test
328 date: Thu Jan 01 00:00:00 1970 +0000
328 date: Thu Jan 01 00:00:00 1970 +0000
329 summary: change foo 2
329 summary: change foo 2
330
330
331 changeset: 0:53245c60e682
331 changeset: 0:53245c60e682
332 user: test
332 user: test
333 date: Thu Jan 01 00:00:00 1970 +0000
333 date: Thu Jan 01 00:00:00 1970 +0000
334 summary: add foo
334 summary: add foo
335
335
336 % qclone
337 main repo:
338 rev 1: change foo
339 rev 0: add foo
340 patch repo:
341 rev 0: checkpoint
342 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
343 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
344 main repo:
345 rev 0: add foo
346 patch repo:
347 rev 0: checkpoint
348 Patch queue now empty
349 main repo:
350 rev 0: add foo
351 patch repo:
352 rev 0: checkpoint
353 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
354 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
355 main repo:
356 rev 0: add foo
357 patch repo:
358 rev 0: checkpoint
@@ -1,40 +1,54 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 cat <<EOF >> $HGRCPATH
3 cat <<EOF >> $HGRCPATH
4 [extensions]
4 [extensions]
5 notify=
5 notify=
6
6
7 [hooks]
7 [hooks]
8 incoming.notify = python:hgext.notify.hook
8 incoming.notify = python:hgext.notify.hook
9
9
10 [notify]
10 [notify]
11 config = $HGTMP/.notify.conf
12 sources = pull
11 sources = pull
13 domain = test.com
14 strip = 3
15 template = Subject: {desc|firstline|strip}\nFrom: {author}\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
16 diffstat = False
12 diffstat = False
17
13
18 [web]
19 baseurl = http://test/
20
21 [usersubs]
14 [usersubs]
22 foo@bar = *
15 foo@bar = *
16
17 [reposubs]
18 * = baz
23 EOF
19 EOF
24
20
25 hg help notify
21 hg help notify
26 hg init a
22 hg init a
27 echo a > a/a
23 echo a > a/a
28 echo % commit
24 echo % commit
29 hg --traceback --cwd a commit -Ama -d '0 0'
25 hg --traceback --cwd a commit -Ama -d '0 0'
30
26
31 echo % clone
27 echo % clone
32 hg --traceback clone a b
28 hg --traceback clone a b
33
29
34 echo a >> a/a
30 echo a >> a/a
35 echo % commit
31 echo % commit
36 hg --traceback --cwd a commit -Amb -d '1 0'
32 hg --traceback --cwd a commit -Amb -d '1 0'
37
33
34 echo '% pull (minimal config)'
35 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
36 -e 's/changeset \([0-9a-f]* \)\?in .*test-notif/changeset \1in test-notif/' \
37 -e 's/^details: .*test-notify/details: test-notify/'
38
39 cat <<EOF >> $HGRCPATH
40 [notify]
41 config = $HGTMP/.notify.conf
42 domain = test.com
43 strip = 3
44 template = Subject: {desc|firstline|strip}\nFrom: {author}\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
45
46 [web]
47 baseurl = http://test/
48 EOF
49
38 echo % pull
50 echo % pull
51 hg --cwd b rollback
39 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
52 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
40 -e 's/changeset \([0-9a-f]*\) in .*/changeset \1/'
53 -e 's/changeset \([0-9a-f]*\) in .*/changeset \1/'
54
@@ -1,33 +1,61 b''
1 notify extension - No help text available
1 notify extension - No help text available
2
2
3 no commands defined
3 no commands defined
4 % commit
4 % commit
5 adding a
5 adding a
6 % clone
6 % clone
7 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
7 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
8 % commit
8 % commit
9 % pull (minimal config)
10 pulling from ../a
11 searching for changes
12 adding changesets
13 adding manifests
14 adding file changes
15 added 1 changesets with 1 changes to 1 files
16 Subject: changeset in test-notify/b: b
17 From: test
18 X-Hg-Notification: changeset 0647d048b600
19 Message-Id:
20 To: baz, foo@bar
21
22 changeset 0647d048b600 in test-notify/b
23 details: test-notify/b?cmd=changeset;node=0647d048b600
24 description:
25 b
26
27 diffs (6 lines):
28
29 diff -r cb9a9f314b8b -r 0647d048b600 a
30 --- a/a Thu Jan 01 00:00:00 1970 +0000
31 +++ b/a Thu Jan 01 00:00:01 1970 +0000
32 @@ -1,1 +1,2 @@ a
33 a
34 +a
35 (run 'hg update' to get a working copy)
9 % pull
36 % pull
37 rolling back last transaction
10 pulling from ../a
38 pulling from ../a
11 searching for changes
39 searching for changes
12 adding changesets
40 adding changesets
13 adding manifests
41 adding manifests
14 adding file changes
42 adding file changes
15 added 1 changesets with 1 changes to 1 files
43 added 1 changesets with 1 changes to 1 files
16 Subject: b
44 Subject: b
17 From: test@test.com
45 From: test@test.com
18 X-Hg-Notification: changeset 0647d048b600
46 X-Hg-Notification: changeset 0647d048b600
19 Message-Id:
47 Message-Id:
20 To: foo@bar
48 To: baz@test.com, foo@bar
21
49
22 changeset 0647d048b600
50 changeset 0647d048b600
23 description:
51 description:
24 b
52 b
25 diffs (6 lines):
53 diffs (6 lines):
26
54
27 diff -r cb9a9f314b8b -r 0647d048b600 a
55 diff -r cb9a9f314b8b -r 0647d048b600 a
28 --- a/a Thu Jan 01 00:00:00 1970 +0000
56 --- a/a Thu Jan 01 00:00:00 1970 +0000
29 +++ b/a Thu Jan 01 00:00:01 1970 +0000
57 +++ b/a Thu Jan 01 00:00:01 1970 +0000
30 @@ -1,1 +1,2 @@ a
58 @@ -1,1 +1,2 @@ a
31 a
59 a
32 +a
60 +a
33 (run 'hg update' to get a working copy)
61 (run 'hg update' to get a working copy)
General Comments 0
You need to be logged in to leave comments. Login now