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