Skip to content

Commit 558c841

Browse files
authored
add the ability to strip outgoing changesets for Mercurial repositories (#4605)
fixes #4602
1 parent 737b69f commit 558c841

File tree

3 files changed

+191
-21
lines changed

3 files changed

+191
-21
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ jobs:
4444
- name: Install Universal ctags (Windows)
4545
if: runner.os == 'Windows'
4646
run: choco install universal-ctags
47-
- name: Before build actions (Unix)
48-
if: runner.os == 'Linux' || runner.os == 'macOS'
47+
- name: Before build actions
48+
shell: bash
4949
run: ./dev/before
5050
- name: Maven build
5151
shell: bash

tools/src/main/python/opengrok_tools/scm/mercurial.py

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ class MercurialRepository(Repository):
3232
def __init__(self, name, logger, path, project, command, env, hooks, timeout):
3333
super().__init__(name, logger, path, project, command, env, hooks, timeout)
3434

35-
self.command = self._repository_command(command, default=lambda: which('hg'))
35+
self.command = self._repository_command(command, default=lambda: which("hg"))
3636

3737
if not self.command:
3838
raise RepositoryException("Cannot get hg command")
3939

4040
def get_branch(self):
4141
hg_command = [self.command, "branch"]
42-
cmd = self.get_command(hg_command, work_dir=self.path,
43-
env_vars=self.env, logger=self.logger)
42+
cmd = self.get_command(
43+
hg_command, work_dir=self.path, env_vars=self.env, logger=self.logger
44+
)
4445
cmd.execute()
4546
self.logger.info("output of {}:".format(cmd))
4647
self.logger.info(cmd.getoutputstr())
@@ -49,12 +50,10 @@ def get_branch(self):
4950
return None
5051
else:
5152
if not cmd.getoutput():
52-
self.logger.error("no output from {}".
53-
format(hg_command))
53+
self.logger.error("no output from {}".format(hg_command))
5454
return None
5555
if len(cmd.getoutput()) == 0:
56-
self.logger.error("empty output from {}".
57-
format(hg_command))
56+
self.logger.error("empty output from {}".format(hg_command))
5857
return None
5958
return cmd.getoutput()[0].strip()
6059

@@ -68,8 +67,9 @@ def reposync(self):
6867
if branch != "default":
6968
hg_command.append("-b")
7069
hg_command.append(branch)
71-
cmd = self.get_command(hg_command, work_dir=self.path,
72-
env_vars=self.env, logger=self.logger)
70+
cmd = self.get_command(
71+
hg_command, work_dir=self.path, env_vars=self.env, logger=self.logger
72+
)
7373
cmd.execute()
7474
self.logger.info("output of {}:".format(cmd))
7575
self.logger.info(cmd.getoutputstr())
@@ -90,10 +90,11 @@ def reposync(self):
9090
# biggest index as this is likely the correct one.
9191
#
9292
hg_command.append("-r")
93-
hg_command.append("max(head() and branch(\".\"))")
93+
hg_command.append('max(head() and branch("."))')
9494

95-
cmd = self.get_command(hg_command, work_dir=self.path,
96-
env_vars=self.env, logger=self.logger)
95+
cmd = self.get_command(
96+
hg_command, work_dir=self.path, env_vars=self.env, logger=self.logger
97+
)
9798
cmd.execute()
9899
self.logger.info("output of {}:".format(cmd))
99100
self.logger.info(cmd.getoutputstr())
@@ -107,25 +108,83 @@ def incoming_check(self):
107108
branch = self.get_branch()
108109
if not branch:
109110
# Error logged already in get_branch().
110-
raise RepositoryException('cannot get branch for repository {}'.
111-
format(self))
111+
raise RepositoryException(
112+
"cannot get branch for repository {}".format(self)
113+
)
112114

113-
hg_command = [self.command, 'incoming']
115+
hg_command = [self.command, "incoming"]
114116
if branch != "default":
115117
hg_command.append("-b")
116118
hg_command.append(branch)
117-
cmd = self.get_command(hg_command, work_dir=self.path,
118-
env_vars=self.env, logger=self.logger)
119+
cmd = self.get_command(
120+
hg_command, work_dir=self.path, env_vars=self.env, logger=self.logger
121+
)
119122
cmd.execute()
120123
self.logger.info("output of {}:".format(cmd))
121124
self.logger.info(cmd.getoutputstr())
122125
retcode = cmd.getretcode()
123126
if cmd.getstate() != Command.FINISHED or retcode not in [0, 1]:
124127
cmd.log_error("failed to perform incoming")
125-
raise RepositoryException('failed to perform incoming command '
126-
'for repository {}'.format(self))
128+
raise RepositoryException(
129+
"failed to perform incoming command " "for repository {}".format(self)
130+
)
127131

128132
if retcode == 0:
129133
return True
130134
else:
131135
return False
136+
137+
def strip_outgoing(self):
138+
"""
139+
Check for outgoing changes and if found, strip them.
140+
:return: True if there were any changes stripped, False otherwise.
141+
"""
142+
#
143+
# Avoid _run_command() as it complains to the log about failed command
144+
# when 'hg out' returns 1 which is legitimate return value.
145+
#
146+
cmd = self.get_command(
147+
[self.command, "out", "-q", "-b", ".", "--template={rev}\\n"],
148+
work_dir=self.path,
149+
env_vars=self.env,
150+
logger=self.logger,
151+
)
152+
cmd.execute()
153+
status = cmd.getretcode()
154+
155+
#
156+
# If there are outgoing changes, 'hg out' returns 0, otherwise returns 1.
157+
# If the 'hg out' command fails for some reason, it will return 255.
158+
# Hence, check for positive value as bail out indication.
159+
#
160+
if status > 0:
161+
return False
162+
163+
revisions = list(filter(None, cmd.getoutputstr().split("\n")))
164+
if len(revisions) == 0:
165+
return False
166+
167+
#
168+
# The revision specification will produce all outgoing changesets.
169+
# The 'hg strip' command will remove them all. Also, the 'strip'
170+
# has become part of core Mercurial, however use the --config to
171+
# enable the extension for backward compatibility.
172+
#
173+
self.logger.debug(
174+
f"Removing outgoing changesets in repository {self}: {revisions}"
175+
)
176+
status, out = self._run_command(
177+
[
178+
self.command,
179+
"--config",
180+
"extensions.strip=",
181+
"strip",
182+
'outgoing() and branch(".")',
183+
]
184+
)
185+
if status != 0:
186+
raise RepositoryException(
187+
f"failed to strip outgoing changesets from {self}"
188+
)
189+
190+
return True
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# CDDL HEADER START
5+
#
6+
# The contents of this file are subject to the terms of the
7+
# Common Development and Distribution License (the "License").
8+
# You may not use this file except in compliance with the License.
9+
#
10+
# See LICENSE.txt included in this distribution for the specific
11+
# language governing permissions and limitations under the License.
12+
#
13+
# When distributing Covered Code, include this CDDL HEADER in each
14+
# file and include the License file at LICENSE.txt.
15+
# If applicable, add the following below this CDDL HEADER, with the
16+
# fields enclosed by brackets "[]" replaced with your own identifying
17+
# information: Portions Copyright [yyyy] [name of copyright owner]
18+
#
19+
# CDDL HEADER END
20+
#
21+
22+
#
23+
# Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
24+
#
25+
26+
import os
27+
import shutil
28+
import tempfile
29+
30+
import pytest
31+
from mockito import mock
32+
from opengrok_tools.scm.mercurial import MercurialRepository
33+
from opengrok_tools.utils.command import Command
34+
35+
36+
def hg_commit_file(file_path, repo_path, comment):
37+
cmd = Command(
38+
[
39+
"hg",
40+
"commit",
41+
"-m",
42+
comment,
43+
"-u",
44+
"Snufkin <snufkin@moominvalley.org>",
45+
file_path,
46+
],
47+
work_dir=repo_path,
48+
)
49+
cmd.execute()
50+
assert cmd.getretcode() == 0
51+
52+
53+
def hg_add_commit_file(file_path, repo_path, comment):
54+
"""
55+
:param file_path: path to the file to be created
56+
:param repo_path: Mercurial repository path
57+
:param comment: content and commit comment
58+
"""
59+
with open(file_path, "w", encoding="ascii") as fp:
60+
fp.write(comment)
61+
assert os.path.exists(file_path)
62+
63+
cmd = Command(["hg", "add", file_path], work_dir=repo_path)
64+
cmd.execute()
65+
assert cmd.getretcode() == 0
66+
67+
hg_commit_file(file_path, repo_path, comment)
68+
69+
70+
@pytest.mark.parametrize("create_file_in_parent", [True, False])
71+
@pytest.mark.skipif(shutil.which("hg") is None, reason="need hg")
72+
def test_strip_outgoing(create_file_in_parent):
73+
with tempfile.TemporaryDirectory() as test_root:
74+
# Initialize Mercurial repository.
75+
repo_parent_path = os.path.join(test_root, "parent")
76+
os.mkdir(repo_parent_path)
77+
cmd = Command(["hg", "init"], work_dir=repo_parent_path)
78+
cmd.execute()
79+
assert cmd.getretcode() == 0
80+
81+
file_name = "foo.txt"
82+
#
83+
# Create a file in the parent repository. This is done so that
84+
# after the strip is done in the cloned repository, the branch
85+
# is still known for 'hg out'. Normally this would be the case.
86+
#
87+
if create_file_in_parent:
88+
file_path = os.path.join(repo_parent_path, file_name)
89+
hg_add_commit_file(file_path, repo_parent_path, "parent")
90+
91+
# Clone the repository and create couple of new changesets.
92+
repo_clone_path = os.path.join(test_root, "clone")
93+
cmd = Command(
94+
["hg", "clone", repo_parent_path, repo_clone_path], work_dir=test_root
95+
)
96+
cmd.execute()
97+
assert cmd.getretcode() == 0
98+
99+
file_path = os.path.join(repo_clone_path, file_name)
100+
hg_add_commit_file(file_path, repo_clone_path, "first")
101+
102+
with open(file_path, "a", encoding="ascii") as fp:
103+
fp.write("bar")
104+
hg_commit_file(file_path, repo_clone_path, "second")
105+
106+
# Strip the changesets.
107+
repository = MercurialRepository(
108+
"hgout", mock(), repo_clone_path, "test-1", None, None, None, None
109+
)
110+
assert repository.strip_outgoing()
111+
assert not repository.strip_outgoing()

0 commit comments

Comments
 (0)