##// END OF EJS Templates
chainsaw-update: taking care of initial cloning...
Georges Racinet -
r52326:d36a81d7 default
parent child Browse files
Show More
@@ -1,157 +1,217 b''
1 1 # chainsaw.py
2 2 #
3 3 # Copyright 2022 Georges Racinet <georges.racinet@octobus.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """chainsaw is a collection of single-minded and dangerous tools. (EXPERIMENTAL)
8 8
9 9 "Don't use a chainsaw to cut your food!"
10 10
11 11 The chainsaw extension provides commands that are so much geared towards a
12 12 specific use case in a specific context or environment that they are totally
13 13 inappropriate and **really dangerous** in other contexts.
14 14
15 15 The help text of each command explicitly summarizes its context of application
16 16 and the wanted end result.
17 17
18 18 It is recommended to run these commands with the ``HGPLAIN`` environment
19 19 variable (see :hg:`help scripting`).
20 20 """
21 21
22 22 import shutil
23 23
24 24 from mercurial.i18n import _
25 25 from mercurial import (
26 26 cmdutil,
27 27 commands,
28 28 error,
29 localrepo,
29 30 registrar,
30 31 )
32 from mercurial.utils import (
33 urlutil,
34 )
31 35
32 36 cmdtable = {}
33 37 command = registrar.command(cmdtable)
34 38 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
35 39 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
36 40 # be specifying the version(s) of Mercurial they are tested with, or
37 41 # leave the attribute unspecified.
38 42 testedwith = b'ships-with-hg-core'
39 43
40 44
41 45 @command(
42 46 b'admin::chainsaw-update',
43 47 [
44 48 (
45 49 b'',
46 50 b'purge-unknown',
47 51 True,
48 52 _(
49 53 b'Remove unversioned files before update. Disabling this can '
50 54 b'in some cases interfere with the update.'
51 55 b'See also :hg:`purge`.'
52 56 ),
53 57 ),
54 58 (
55 59 b'',
56 60 b'purge-ignored',
57 61 True,
58 62 _(
59 63 b'Remove ignored files before update. Disable this for '
60 64 b'instance to reuse previous compiler object files. '
61 65 b'See also :hg:`purge`.'
62 66 ),
63 67 ),
64 68 (
65 69 b'',
66 70 b'rev',
67 71 b'',
68 72 _(b'revision to update to'),
69 73 ),
70 74 (
71 75 b'',
72 76 b'source',
73 77 b'',
74 78 _(b'repository to clone from'),
75 79 ),
80 (
81 b'',
82 b'dest',
83 b'',
84 _(b'repository to update to REV (possibly cloning)'),
85 ),
86 (
87 b'',
88 b'initial-clone-minimal',
89 False,
90 _(
91 b'Pull only the prescribed revision upon initial cloning. '
92 b'This has the side effect of ignoring clone-bundles, '
93 b'which if often slower on the client side and stressful '
94 b'to the server than applying available clone bundles.'
95 ),
96 ),
76 97 ],
77 _(b'hg admin::chainsaw-update [OPTION] --rev REV --source SOURCE...'),
98 _(
99 b'hg admin::chainsaw-update [OPTION] --rev REV --source SOURCE --dest DEST'
100 ),
78 101 helpbasic=True,
102 norepo=True,
79 103 )
80 def update(ui, repo, **opts):
104 def update(ui, **opts):
81 105 """pull and update to a given revision, no matter what, (EXPERIMENTAL)
82 106
83 107 Context of application: *some* Continuous Integration (CI) systems,
84 108 packaging or deployment tools.
85 109
86 Wanted end result: clean working directory updated at the given revision.
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
112 updated at the given revision.
87 113
88 114 chainsaw-update pulls from one source, then updates the working directory
89 115 to the given revision, overcoming anything that would stand in the way.
90 116
91 117 By default, it will:
92 118
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.
121 The initial clone is full by default, so that clonebundles can be
122 applied. Use the --initial-clone-minimal flag to avoid this.
93 123 - break locks if needed, leading to possible corruption if there
94 124 is a concurrent write access.
95 125 - perform recovery actions if needed
96 126 - revert any local modification.
97 127 - purge unknown and ignored files.
98 128 - go as far as to reclone if everything else failed (not implemented yet).
99 129
100 130 DO NOT use it for anything else than performing a series
101 131 of unattended updates, with full exclusive repository access each time
102 132 and without any other local work than running build scripts.
103 133 In case the local repository is a share (see :hg:`help share`), exclusive
104 134 write access to the share source is also mandatory.
105 135
106 136 It is recommended to run these commands with the ``HGPLAIN`` environment
107 137 variable (see :hg:`scripting`).
108 138
109 139 Motivation: in Continuous Integration and Delivery systems (CI/CD), the
110 140 occasional remnant or bogus lock are common sources of waste of time (both
111 141 working time and calendar time). CI/CD scripts tend to grow with counter-
112 142 measures, often done in urgency. Also, whilst it is neat to keep
113 143 repositories from one job to the next (especially with large
114 144 repositories), an exceptional recloning is better than missing a release
115 145 deadline.
116 146 """
117 147 rev = opts['rev']
118 148 source = opts['source']
149 repo_path = opts['dest']
119 150 if not rev:
120 151 raise error.InputError(_(b'specify a target revision with --rev'))
121 152 if not source:
122 153 raise error.InputError(_(b'specify a pull path with --source'))
154 if not repo_path:
155 raise error.InputError(_(b'specify a repo path with --dest'))
156 repo_path = urlutil.urllocalpath(repo_path)
157
158 try:
159 repo = localrepo.instance(ui, repo_path, create=False)
160 repo_created = False
161 ui.status(_(b'loaded repository at "%s"\n' % repo_path))
162 except error.RepoError:
163 try:
164 shutil.rmtree(repo_path)
165 except FileNotFoundError:
166 ui.status(_(b'no such directory: "%s"\n' % repo_path))
167 else:
168 ui.status(
169 _(
170 b'removed non-repository file or directory '
171 b'at "%s"' % repo_path
172 )
173 )
174
175 ui.status(_(b'creating repository at "%s"\n' % repo_path))
176 repo = localrepo.instance(ui, repo_path, create=True)
177 repo_created = True
178
123 179 if repo.svfs.tryunlink(b'lock'):
124 180 ui.status(_(b'had to break store lock\n'))
125 181 if repo.vfs.tryunlink(b'wlock'):
126 182 ui.status(_(b'had to break working copy lock\n'))
127 183
128 184 ui.status(_(b'recovering after interrupted transaction, if any\n'))
129 185 repo.recover()
130 186
131 187 ui.status(_(b'pulling from %s\n') % source)
188 if repo_created and not opts.get('initial_clone_minimal'):
189 pull_revs = []
190 else:
191 pull_revs = [rev]
132 192 overrides = {(b'ui', b'quiet'): True}
133 with ui.configoverride(overrides, b'chainsaw-update'):
193 with repo.ui.configoverride(overrides, b'chainsaw-update'):
134 194 pull = cmdutil.findcmd(b'pull', commands.table)[1][0]
135 pull(ui, repo, source, rev=[rev], remote_hidden=False)
195 pull(repo.ui, repo, source, rev=pull_revs, remote_hidden=False)
136 196
137 197 purge = cmdutil.findcmd(b'purge', commands.table)[1][0]
138 198 purge(
139 199 ui,
140 200 repo,
141 201 dirs=True,
142 202 all=opts.get('purge_ignored'),
143 203 files=opts.get('purge_unknown'),
144 204 confirm=False,
145 205 )
146 206
147 207 ui.status(_(b'updating to revision \'%s\'\n') % rev)
148 208 update = cmdutil.findcmd(b'update', commands.table)[1][0]
149 209 update(ui, repo, rev=rev, clean=True)
150 210
151 211 ui.status(
152 212 _(
153 213 b'chainsaw-update to revision \'%s\' '
154 214 b'for repository at \'%s\' done\n'
155 215 )
156 216 % (rev, repo.root)
157 217 )
@@ -1,169 +1,255 b''
1 1 ============================================
2 2 Tests for the admin::chainsaw-update command
3 3 ============================================
4 4
5 5 setup
6 6 =====
7 7
8 8 $ cat >> $HGRCPATH << EOF
9 9 > [extensions]
10 10 > chainsaw=
11 11 > EOF
12 12
13 13 $ hg init src
14 14 $ cd src
15 15 $ echo 1 > root
16 16 $ hg add root
17 17 $ hg ci -Am R_0
18 18 $ hg branch A
19 19 marked working directory as branch A
20 20 (branches are permanent and global, did you want a bookmark?)
21 21 $ echo 42 > bar
22 22 $ hg add bar
23 23 $ hg ci -Am A_0
24 24 $ echo 1337 > bar
25 25 $ hg ci -Am A_1
26 26 $ hg update 'desc(R_0)'
27 27 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
28 28 $ echo 1 > foo
29 29 $ hg add foo
30 30 $ hg ci -Am B_0
31 31 $ hg log -G
32 32 @ changeset: 3:bfcb8e629987
33 33 | tag: tip
34 34 | parent: 0:06f48e4098b8
35 35 | user: test
36 36 | date: Thu Jan 01 00:00:00 1970 +0000
37 37 | summary: B_0
38 38 |
39 39 | o changeset: 2:7fd8de258aa4
40 40 | | branch: A
41 41 | | user: test
42 42 | | date: Thu Jan 01 00:00:00 1970 +0000
43 43 | | summary: A_1
44 44 | |
45 45 | o changeset: 1:ae1692b8aadb
46 46 |/ branch: A
47 47 | user: test
48 48 | date: Thu Jan 01 00:00:00 1970 +0000
49 49 | summary: A_0
50 50 |
51 51 o changeset: 0:06f48e4098b8
52 52 user: test
53 53 date: Thu Jan 01 00:00:00 1970 +0000
54 54 summary: R_0
55 55
56 56 $ cd ..
57 57
58 58 Actual tests
59 59 ============
60 60
61 Simple invocation
62 -----------------
61 Initial cloning if needed
62 -------------------------
63 63
64 $ hg init repo
65 $ cd repo
66 $ hg admin::chainsaw-update --rev default --source ../src
64 $ hg admin::chainsaw-update --dest repo --rev default --source ./src
65 no such directory: "repo"
66 creating repository at "repo"
67 67 recovering after interrupted transaction, if any
68 68 no interrupted transaction available
69 pulling from ../src
69 pulling from ./src
70 70 updating to revision 'default'
71 71 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
72 72 chainsaw-update to revision 'default' for repository at '$TESTTMP/repo' done
73 73
74 $ cd repo
74 75 $ hg log -G
75 @ changeset: 1:bfcb8e629987
76 @ changeset: 3:bfcb8e629987
76 77 | tag: tip
78 | parent: 0:06f48e4098b8
77 79 | user: test
78 80 | date: Thu Jan 01 00:00:00 1970 +0000
79 81 | summary: B_0
80 82 |
83 | o changeset: 2:7fd8de258aa4
84 | | branch: A
85 | | user: test
86 | | date: Thu Jan 01 00:00:00 1970 +0000
87 | | summary: A_1
88 | |
89 | o changeset: 1:ae1692b8aadb
90 |/ branch: A
91 | user: test
92 | date: Thu Jan 01 00:00:00 1970 +0000
93 | summary: A_0
94 |
81 95 o changeset: 0:06f48e4098b8
82 96 user: test
83 97 date: Thu Jan 01 00:00:00 1970 +0000
84 98 summary: R_0
85 99
86 100 $ hg status -A
87 101 C foo
88 102 C root
89 103 $ cat foo
90 104 1
91 105
92 106 Test lock breacking capabilities
93 107 --------------------------------
94 108
95 109 Demonstrate lock-breaking capabilities with locks that regular Mercurial
96 110 operation would not break, because the hostnames registered in locks differ
97 111 from the current hostname (happens a lot with succesive containers):
98 112
99 113 $ ln -s invalid.host.test/effffffc:171814 .hg/store/lock
100 114 $ ln -s invalid.host.test/effffffc:171814 .hg/wlock
101 115 $ hg debuglock
102 116 lock: (.*?), process 171814, host invalid.host.test/effffffc \((\d+)s\) (re)
103 117 wlock: (.*?), process 171814, host invalid.host.test/effffffc \((\d+)s\) (re)
104 118 [2]
105 119
106 $ hg admin::chainsaw-update --no-purge-ignored --rev default --source ../src
120 $ hg admin::chainsaw-update --no-purge-ignored --dest . --rev default --source ../src
121 loaded repository at "."
107 122 had to break store lock
108 123 had to break working copy lock
109 124 recovering after interrupted transaction, if any
110 125 no interrupted transaction available
111 126 pulling from ../src
112 127 updating to revision 'default'
113 128 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
114 129 chainsaw-update to revision 'default' for repository at '$TESTTMP/repo' done
115 130
116 131 Test file purging capabilities
117 132 ------------------------------
118 133
119 134 Let's also add local modifications (tracked and untracked) to demonstrate the
120 135 purging.
121 136
122 137 $ echo untracked > bar
123 138 $ echo modified > foo
124 139 $ hg status -A
125 140 M foo
126 141 ? bar
127 142 C root
128 143
129 144 $ echo 2 > ../src/foo
130 $ hg -R ../src commit -m2
131 $ hg admin::chainsaw-update --rev default --source ../src -q
145 $ hg -R ../src commit -mB_1
146 $ hg admin::chainsaw-update --dest . --rev default --source ../src -q
132 147 no interrupted transaction available
148 $ hg log -G
149 @ changeset: 4:973ab81c95fb
150 | tag: tip
151 | user: test
152 | date: Thu Jan 01 00:00:00 1970 +0000
153 | summary: B_1
154 |
155 o changeset: 3:bfcb8e629987
156 | parent: 0:06f48e4098b8
157 | user: test
158 | date: Thu Jan 01 00:00:00 1970 +0000
159 | summary: B_0
160 |
161 | o changeset: 2:7fd8de258aa4
162 | | branch: A
163 | | user: test
164 | | date: Thu Jan 01 00:00:00 1970 +0000
165 | | summary: A_1
166 | |
167 | o changeset: 1:ae1692b8aadb
168 |/ branch: A
169 | user: test
170 | date: Thu Jan 01 00:00:00 1970 +0000
171 | summary: A_0
172 |
173 o changeset: 0:06f48e4098b8
174 user: test
175 date: Thu Jan 01 00:00:00 1970 +0000
176 summary: R_0
177
133 178 $ hg status -A
134 179 C foo
135 180 C root
136 181 $ cat foo
137 182 2
138 183
139 184 Now behaviour with respect to ignored files: they are not purged if
140 185 the --no-purge-ignored flag is passed, but they are purged by default
141 186
142 187 $ echo bar > .hgignore
143 188 $ hg ci -Aqm hgignore
144 189 $ echo ignored > bar
145 190 $ hg status --all
146 191 I bar
147 192 C .hgignore
148 193 C foo
149 194 C root
150 195
151 $ hg admin::chainsaw-update --no-purge-ignored --rev default --source ../src -q
196 $ hg admin::chainsaw-update --no-purge-ignored --dest . --rev default --source ../src -q
152 197 no interrupted transaction available
153 198 $ hg status --all
154 199 I bar
155 200 C .hgignore
156 201 C foo
157 202 C root
158 203 $ cat bar
159 204 ignored
160 205
161 $ hg admin::chainsaw-update --rev default --source ../src -q
206 $ hg admin::chainsaw-update --dest . --rev default --source ../src -q
162 207 no interrupted transaction available
163 208 $ hg status --all
164 209 C .hgignore
165 210 C foo
166 211 C root
167 212 $ test -f bar
168 213 [1]
169 214
215 test --minimal-initial-cloning variant
216 --------------------------------------
217
218 With `--minimal-initial-cloning`, there is no "requesting all changes"
219 message. Hence clone bundles would be bypassed (TODO test both cases
220 # with an actual clone-bundle)
221
222 $ cd ..
223 $ hg admin::chainsaw-update --dest repo2 --rev default --source src --initial-clone-minimal
224 no such directory: "repo2"
225 creating repository at "repo2"
226 recovering after interrupted transaction, if any
227 no interrupted transaction available
228 pulling from src
229 updating to revision 'default'
230 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
231 chainsaw-update to revision 'default' for repository at '$TESTTMP/repo2' done
232
233 $ cd repo2
234 $ hg log -G
235 @ changeset: 2:973ab81c95fb
236 | tag: tip
237 | user: test
238 | date: Thu Jan 01 00:00:00 1970 +0000
239 | summary: B_1
240 |
241 o changeset: 1:bfcb8e629987
242 | user: test
243 | date: Thu Jan 01 00:00:00 1970 +0000
244 | summary: B_0
245 |
246 o changeset: 0:06f48e4098b8
247 user: test
248 date: Thu Jan 01 00:00:00 1970 +0000
249 summary: R_0
250
251 $ hg status -A
252 C foo
253 C root
254 $ cat foo
255 2
General Comments 0
You need to be logged in to leave comments. Login now