##// END OF EJS Templates
chainsaw-update: exit early if one of the intermediate command fails...
marmoute -
r52328:ad106653 default
parent child Browse files
Show More
@@ -1,219 +1,231 b''
1 # chainsaw.py
1 # chainsaw.py
2 #
2 #
3 # Copyright 2022 Georges Racinet <georges.racinet@octobus.net>
3 # Copyright 2022 Georges Racinet <georges.racinet@octobus.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """chainsaw is a collection of single-minded and dangerous tools. (EXPERIMENTAL)
7 """chainsaw is a collection of single-minded and dangerous tools. (EXPERIMENTAL)
8
8
9 "Don't use a chainsaw to cut your food!"
9 "Don't use a chainsaw to cut your food!"
10
10
11 The chainsaw extension provides commands that are so much geared towards a
11 The chainsaw extension provides commands that are so much geared towards a
12 specific use case in a specific context or environment that they are totally
12 specific use case in a specific context or environment that they are totally
13 inappropriate and **really dangerous** in other contexts.
13 inappropriate and **really dangerous** in other contexts.
14
14
15 The help text of each command explicitly summarizes its context of application
15 The help text of each command explicitly summarizes its context of application
16 and the wanted end result.
16 and the wanted end result.
17
17
18 It is recommended to run these commands with the ``HGPLAIN`` environment
18 It is recommended to run these commands with the ``HGPLAIN`` environment
19 variable (see :hg:`help scripting`).
19 variable (see :hg:`help scripting`).
20 """
20 """
21
21
22 import shutil
22 import shutil
23
23
24 from mercurial.i18n import _
24 from mercurial.i18n import _
25 from mercurial import (
25 from mercurial import (
26 cmdutil,
26 cmdutil,
27 commands,
27 commands,
28 error,
28 error,
29 localrepo,
29 localrepo,
30 registrar,
30 registrar,
31 )
31 )
32 from mercurial.utils import (
32 from mercurial.utils import (
33 urlutil,
33 urlutil,
34 )
34 )
35
35
36 cmdtable = {}
36 cmdtable = {}
37 command = registrar.command(cmdtable)
37 command = registrar.command(cmdtable)
38 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
38 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
39 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
39 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
40 # be specifying the version(s) of Mercurial they are tested with, or
40 # be specifying the version(s) of Mercurial they are tested with, or
41 # leave the attribute unspecified.
41 # leave the attribute unspecified.
42 testedwith = b'ships-with-hg-core'
42 testedwith = b'ships-with-hg-core'
43
43
44
44
45 @command(
45 @command(
46 b'admin::chainsaw-update',
46 b'admin::chainsaw-update',
47 [
47 [
48 (
48 (
49 b'',
49 b'',
50 b'purge-unknown',
50 b'purge-unknown',
51 True,
51 True,
52 _(
52 _(
53 b'Remove unversioned files before update. Disabling this can '
53 b'Remove unversioned files before update. Disabling this can '
54 b'in some cases interfere with the update.'
54 b'in some cases interfere with the update.'
55 b'See also :hg:`purge`.'
55 b'See also :hg:`purge`.'
56 ),
56 ),
57 ),
57 ),
58 (
58 (
59 b'',
59 b'',
60 b'purge-ignored',
60 b'purge-ignored',
61 True,
61 True,
62 _(
62 _(
63 b'Remove ignored files before update. Disable this for '
63 b'Remove ignored files before update. Disable this for '
64 b'instance to reuse previous compiler object files. '
64 b'instance to reuse previous compiler object files. '
65 b'See also :hg:`purge`.'
65 b'See also :hg:`purge`.'
66 ),
66 ),
67 ),
67 ),
68 (
68 (
69 b'',
69 b'',
70 b'rev',
70 b'rev',
71 b'',
71 b'',
72 _(b'revision to update to'),
72 _(b'revision to update to'),
73 ),
73 ),
74 (
74 (
75 b'',
75 b'',
76 b'source',
76 b'source',
77 b'',
77 b'',
78 _(b'repository to clone from'),
78 _(b'repository to clone from'),
79 ),
79 ),
80 (
80 (
81 b'',
81 b'',
82 b'dest',
82 b'dest',
83 b'',
83 b'',
84 _(b'repository to update to REV (possibly cloning)'),
84 _(b'repository to update to REV (possibly cloning)'),
85 ),
85 ),
86 (
86 (
87 b'',
87 b'',
88 b'initial-clone-minimal',
88 b'initial-clone-minimal',
89 False,
89 False,
90 _(
90 _(
91 b'Pull only the prescribed revision upon initial cloning. '
91 b'Pull only the prescribed revision upon initial cloning. '
92 b'This has the side effect of ignoring clone-bundles, '
92 b'This has the side effect of ignoring clone-bundles, '
93 b'which if often slower on the client side and stressful '
93 b'which if often slower on the client side and stressful '
94 b'to the server than applying available clone bundles.'
94 b'to the server than applying available clone bundles.'
95 ),
95 ),
96 ),
96 ),
97 ],
97 ],
98 _(
98 _(
99 b'hg admin::chainsaw-update [OPTION] --rev REV --source SOURCE --dest DEST'
99 b'hg admin::chainsaw-update [OPTION] --rev REV --source SOURCE --dest DEST'
100 ),
100 ),
101 helpbasic=True,
101 helpbasic=True,
102 norepo=True,
102 norepo=True,
103 )
103 )
104 def update(ui, **opts):
104 def update(ui, **opts):
105 """pull and update to a given revision, no matter what, (EXPERIMENTAL)
105 """pull and update to a given revision, no matter what, (EXPERIMENTAL)
106
106
107 Context of application: *some* Continuous Integration (CI) systems,
107 Context of application: *some* Continuous Integration (CI) systems,
108 packaging or deployment tools.
108 packaging or deployment tools.
109
109
110 Wanted end result: local repository at the given REPO_PATH, having the
110 Wanted end result: local repository at the given REPO_PATH, having the
111 latest changes to the given revision and with a clean working directory
111 latest changes to the given revision and with a clean working directory
112 updated at the given revision.
112 updated at the given revision.
113
113
114 chainsaw-update pulls from one source, then updates the working directory
114 chainsaw-update pulls from one source, then updates the working directory
115 to the given revision, overcoming anything that would stand in the way.
115 to the given revision, overcoming anything that would stand in the way.
116
116
117 By default, it will:
117 By default, it will:
118
118
119 - clone if the local repo does not exist yet, **removing any directory
119 - clone if the local repo does not exist yet, **removing any directory
120 at the given path** that would not be a Mercurial repository.
120 at the given path** that would not be a Mercurial repository.
121 The initial clone is full by default, so that clonebundles can be
121 The initial clone is full by default, so that clonebundles can be
122 applied. Use the --initial-clone-minimal flag to avoid this.
122 applied. Use the --initial-clone-minimal flag to avoid this.
123 - break locks if needed, leading to possible corruption if there
123 - break locks if needed, leading to possible corruption if there
124 is a concurrent write access.
124 is a concurrent write access.
125 - perform recovery actions if needed
125 - perform recovery actions if needed
126 - revert any local modification.
126 - revert any local modification.
127 - purge unknown and ignored files.
127 - purge unknown and ignored files.
128 - go as far as to reclone if everything else failed (not implemented yet).
128 - go as far as to reclone if everything else failed (not implemented yet).
129
129
130 DO NOT use it for anything else than performing a series
130 DO NOT use it for anything else than performing a series
131 of unattended updates, with full exclusive repository access each time
131 of unattended updates, with full exclusive repository access each time
132 and without any other local work than running build scripts.
132 and without any other local work than running build scripts.
133 In case the local repository is a share (see :hg:`help share`), exclusive
133 In case the local repository is a share (see :hg:`help share`), exclusive
134 write access to the share source is also mandatory.
134 write access to the share source is also mandatory.
135
135
136 It is recommended to run these commands with the ``HGPLAIN`` environment
136 It is recommended to run these commands with the ``HGPLAIN`` environment
137 variable (see :hg:`scripting`).
137 variable (see :hg:`scripting`).
138
138
139 Motivation: in Continuous Integration and Delivery systems (CI/CD), the
139 Motivation: in Continuous Integration and Delivery systems (CI/CD), the
140 occasional remnant or bogus lock are common sources of waste of time (both
140 occasional remnant or bogus lock are common sources of waste of time (both
141 working time and calendar time). CI/CD scripts tend to grow with counter-
141 working time and calendar time). CI/CD scripts tend to grow with counter-
142 measures, often done in urgency. Also, whilst it is neat to keep
142 measures, often done in urgency. Also, whilst it is neat to keep
143 repositories from one job to the next (especially with large
143 repositories from one job to the next (especially with large
144 repositories), an exceptional recloning is better than missing a release
144 repositories), an exceptional recloning is better than missing a release
145 deadline.
145 deadline.
146 """
146 """
147 rev = opts['rev']
147 rev = opts['rev']
148 source = opts['source']
148 source = opts['source']
149 repo_path = opts['dest']
149 repo_path = opts['dest']
150 if not rev:
150 if not rev:
151 raise error.InputError(_(b'specify a target revision with --rev'))
151 raise error.InputError(_(b'specify a target revision with --rev'))
152 if not source:
152 if not source:
153 raise error.InputError(_(b'specify a pull path with --source'))
153 raise error.InputError(_(b'specify a pull path with --source'))
154 if not repo_path:
154 if not repo_path:
155 raise error.InputError(_(b'specify a repo path with --dest'))
155 raise error.InputError(_(b'specify a repo path with --dest'))
156 repo_path = urlutil.urllocalpath(repo_path)
156 repo_path = urlutil.urllocalpath(repo_path)
157
157
158 try:
158 try:
159 repo = localrepo.instance(ui, repo_path, create=False)
159 repo = localrepo.instance(ui, repo_path, create=False)
160 repo_created = False
160 repo_created = False
161 ui.status(_(b'loaded repository at "%s"\n' % repo_path))
161 ui.status(_(b'loaded repository at "%s"\n' % repo_path))
162 except error.RepoError:
162 except error.RepoError:
163 try:
163 try:
164 shutil.rmtree(repo_path)
164 shutil.rmtree(repo_path)
165 except FileNotFoundError:
165 except FileNotFoundError:
166 ui.status(_(b'no such directory: "%s"\n' % repo_path))
166 ui.status(_(b'no such directory: "%s"\n' % repo_path))
167 else:
167 else:
168 ui.status(
168 ui.status(
169 _(
169 _(
170 b'removed non-repository file or directory '
170 b'removed non-repository file or directory '
171 b'at "%s"' % repo_path
171 b'at "%s"' % repo_path
172 )
172 )
173 )
173 )
174
174
175 ui.status(_(b'creating repository at "%s"\n' % repo_path))
175 ui.status(_(b'creating repository at "%s"\n' % repo_path))
176 repo = localrepo.instance(ui, repo_path, create=True)
176 repo = localrepo.instance(ui, repo_path, create=True)
177 repo_created = True
177 repo_created = True
178
178
179 if repo.svfs.tryunlink(b'lock'):
179 if repo.svfs.tryunlink(b'lock'):
180 ui.status(_(b'had to break store lock\n'))
180 ui.status(_(b'had to break store lock\n'))
181 if repo.vfs.tryunlink(b'wlock'):
181 if repo.vfs.tryunlink(b'wlock'):
182 ui.status(_(b'had to break working copy lock\n'))
182 ui.status(_(b'had to break working copy lock\n'))
183 # If another process relock after the breacking above, the next locking
183 # If another process relock after the breacking above, the next locking
184 # will have to wait.
184 # will have to wait.
185 with repo.wlock(), repo.lock():
185 with repo.wlock(), repo.lock():
186 ui.status(_(b'recovering after interrupted transaction, if any\n'))
186 ui.status(_(b'recovering after interrupted transaction, if any\n'))
187 repo.recover()
187 repo.recover()
188
188
189 ui.status(_(b'pulling from %s\n') % source)
189 ui.status(_(b'pulling from %s\n') % source)
190 if repo_created and not opts.get('initial_clone_minimal'):
190 if repo_created and not opts.get('initial_clone_minimal'):
191 pull_revs = []
191 pull_revs = []
192 else:
192 else:
193 pull_revs = [rev]
193 pull_revs = [rev]
194 overrides = {(b'ui', b'quiet'): True}
194 overrides = {(b'ui', b'quiet'): True}
195 with repo.ui.configoverride(overrides, b'chainsaw-update'):
195 with repo.ui.configoverride(overrides, b'chainsaw-update'):
196 pull = cmdutil.findcmd(b'pull', commands.table)[1][0]
196 pull = cmdutil.findcmd(b'pull', commands.table)[1][0]
197 pull(repo.ui, repo, source, rev=pull_revs, remote_hidden=False)
197 ret = pull(
198 repo.ui,
199 repo,
200 source,
201 rev=pull_revs,
202 remote_hidden=False,
203 )
204 if ret:
205 return ret
198
206
199 purge = cmdutil.findcmd(b'purge', commands.table)[1][0]
207 purge = cmdutil.findcmd(b'purge', commands.table)[1][0]
200 purge(
208 ret = purge(
201 ui,
209 ui,
202 repo,
210 repo,
203 dirs=True,
211 dirs=True,
204 all=opts.get('purge_ignored'),
212 all=opts.get('purge_ignored'),
205 files=opts.get('purge_unknown'),
213 files=opts.get('purge_unknown'),
206 confirm=False,
214 confirm=False,
207 )
215 )
216 if ret:
217 return ret
208
218
209 ui.status(_(b'updating to revision \'%s\'\n') % rev)
219 ui.status(_(b'updating to revision \'%s\'\n') % rev)
210 update = cmdutil.findcmd(b'update', commands.table)[1][0]
220 update = cmdutil.findcmd(b'update', commands.table)[1][0]
211 update(ui, repo, rev=rev, clean=True)
221 ret = update(ui, repo, rev=rev, clean=True)
222 if ret:
223 return ret
212
224
213 ui.status(
225 ui.status(
214 _(
226 _(
215 b'chainsaw-update to revision \'%s\' '
227 b'chainsaw-update to revision \'%s\' '
216 b'for repository at \'%s\' done\n'
228 b'for repository at \'%s\' done\n'
217 )
229 )
218 % (rev, repo.root)
230 % (rev, repo.root)
219 )
231 )
General Comments 0
You need to be logged in to leave comments. Login now