1import ConfigParser
2import json
3import os
4
5from mozharness.base.errors import HgErrorList
6from mozharness.base.log import FATAL, INFO
7from mozharness.base.vcs.mercurial import MercurialVCS
8
9
10class MercurialRepoManipulationMixin(object):
11
12    def get_version(self, repo_root,
13                    version_file="browser/config/version.txt"):
14        version_path = os.path.join(repo_root, version_file)
15        contents = self.read_from_file(version_path, error_level=FATAL)
16        lines = [l for l in contents.splitlines() if l and
17                 not l.startswith("#")]
18        return lines[-1].split(".")
19
20    def replace(self, file_name, from_, to_):
21        """ Replace text in a file.
22            """
23        text = self.read_from_file(file_name, error_level=FATAL)
24        new_text = text.replace(from_, to_)
25        if text == new_text:
26            self.fatal("Cannot replace '%s' to '%s' in '%s'" %
27                       (from_, to_, file_name))
28        self.write_to_file(file_name, new_text, error_level=FATAL)
29
30    def query_hg_revision(self, path):
31        """ Avoid making 'pull' a required action every run, by being able
32            to fall back to figuring out the revision from the cloned repo
33            """
34        m = MercurialVCS(log_obj=self.log_obj, config=self.config)
35        revision = m.get_revision_from_path(path)
36        return revision
37
38    def hg_commit(self, cwd, message, user=None, ignore_no_changes=False):
39        """ Commit changes to hg.
40            """
41        cmd = self.query_exe('hg', return_type='list') + [
42            'commit', '-m', message]
43        if user:
44            cmd.extend(['-u', user])
45        success_codes = [0]
46        if ignore_no_changes:
47            success_codes.append(1)
48        self.run_command(
49            cmd, cwd=cwd, error_list=HgErrorList,
50            halt_on_failure=True,
51            success_codes=success_codes
52        )
53        return self.query_hg_revision(cwd)
54
55    def clean_repos(self):
56        """ We may end up with contaminated local repos at some point, but
57            we don't want to have to clobber and reclone from scratch every
58            time.
59
60            This is an attempt to clean up the local repos without needing a
61            clobber.
62            """
63        dirs = self.query_abs_dirs()
64        hg = self.query_exe("hg", return_type="list")
65        hg_repos = self.query_repos()
66        hg_strip_error_list = [{
67            'substr': r'''abort: empty revision set''', 'level': INFO,
68            'explanation': "Nothing to clean up; we're good!",
69        }] + HgErrorList
70        for repo_config in hg_repos:
71            repo_name = repo_config["dest"]
72            repo_path = os.path.join(dirs['abs_work_dir'], repo_name)
73            if os.path.exists(repo_path):
74                # hg up -C to discard uncommitted changes
75                self.run_command(
76                    hg + ["up", "-C", "-r", repo_config['branch']],
77                    cwd=repo_path,
78                    error_list=HgErrorList,
79                    halt_on_failure=True,
80                )
81                # discard unpushed commits
82                status = self.retry(
83                    self.run_command,
84                    args=(hg + ["--config", "extensions.mq=", "strip",
85                          "--no-backup", "outgoing()"], ),
86                    kwargs={
87                        'cwd': repo_path,
88                        'error_list': hg_strip_error_list,
89                        'return_type': 'num_errors',
90                        'success_codes': (0, 255),
91                    },
92                )
93                if status not in [0, 255]:
94                    self.fatal("Issues stripping outgoing revisions!")
95                # 2nd hg up -C to make sure we're not on a stranded head
96                # which can happen when reverting debugsetparents
97                self.run_command(
98                    hg + ["up", "-C", "-r", repo_config['branch']],
99                    cwd=repo_path,
100                    error_list=HgErrorList,
101                    halt_on_failure=True,
102                )
103
104    def commit_changes(self):
105        """ Do the commit.
106            """
107        hg = self.query_exe("hg", return_type="list")
108        for cwd in self.query_commit_dirs():
109            self.run_command(hg + ["diff"], cwd=cwd)
110            self.hg_commit(
111                cwd, user=self.config['hg_user'],
112                message=self.query_commit_message(),
113                ignore_no_changes=self.config.get("ignore_no_changes", False)
114            )
115        self.info("Now verify |hg out| and |hg out --patch| if you're paranoid, and --push")
116
117    def hg_tag(self, cwd, tags, user=None, message=None, revision=None,
118               force=None, halt_on_failure=True):
119        if isinstance(tags, basestring):
120            tags = [tags]
121        cmd = self.query_exe('hg', return_type='list') + ['tag']
122        if not message:
123            message = "No bug - Tagging %s" % os.path.basename(cwd)
124            if revision:
125                message = "%s %s" % (message, revision)
126            message = "%s with %s" % (message, ', '.join(tags))
127            message += " a=release DONTBUILD CLOSED TREE"
128        self.info(message)
129        cmd.extend(['-m', message])
130        if user:
131            cmd.extend(['-u', user])
132        if revision:
133            cmd.extend(['-r', revision])
134        if force:
135            cmd.append('-f')
136        cmd.extend(tags)
137        return self.run_command(
138            cmd, cwd=cwd, halt_on_failure=halt_on_failure,
139            error_list=HgErrorList
140        )
141
142    def query_existing_tags(self, cwd, halt_on_failure=True):
143        cmd = self.query_exe('hg', return_type='list') + ['tags']
144        existing_tags = {}
145        output = self.get_output_from_command(
146            cmd, cwd=cwd, halt_on_failure=halt_on_failure
147        )
148        for line in output.splitlines():
149            parts = line.split(' ')
150            if len(parts) > 1:
151                # existing_tags = {TAG: REVISION, ...}
152                existing_tags[parts[0]] = parts[-1].split(':')[-1]
153        self.info(
154            "existing_tags:\n{}".format(
155                json.dumps(existing_tags, sort_keys=True, indent=4)
156            )
157        )
158        return existing_tags
159
160    def push(self):
161        """
162            """
163        error_message = """Push failed!  If there was a push race, try rerunning
164the script (--clean-repos --pull --migrate).  The second run will be faster."""
165        hg = self.query_exe("hg", return_type="list")
166        for cwd in self.query_push_dirs():
167            if not cwd:
168                self.warning("Skipping %s" % cwd)
169                continue
170            push_cmd = hg + ['push'] + self.query_push_args(cwd)
171            if self.config.get("push_dest"):
172                push_cmd.append(self.config["push_dest"])
173            status = self.run_command(
174                push_cmd,
175                cwd=cwd,
176                error_list=HgErrorList,
177                success_codes=[0, 1],
178            )
179            if status == 1:
180                self.warning("No changes for %s!" % cwd)
181            elif status:
182                self.fatal(error_message)
183
184    def edit_repo_hg_rc(self, cwd, section, key, value):
185        hg_rc = self.read_repo_hg_rc(cwd)
186        hg_rc.set(section, key, value)
187
188        with open(self._get_hg_rc_path(cwd), 'wb') as f:
189            hg_rc.write(f)
190
191    def read_repo_hg_rc(self, cwd):
192        hg_rc = ConfigParser.ConfigParser()
193        hg_rc.read(self._get_hg_rc_path(cwd))
194        return hg_rc
195
196    def _get_hg_rc_path(self, cwd):
197        return os.path.join(cwd, '.hg', 'hgrc')
198