def call(self, hook_input=None, hook_args=None, cwd=None): """Call the script specified via self.hook_option_name. This method assumes that the repository's configuration defines a hook via the self.hook_option_name option. It calls that hook with the given argument. Raises InvalidUpdate if we failed to call the hook for whatever reason (typically, the hook's path does not exist, or we do not have the right permissions for us to execute it). PARAMETERS hook_input: A string, containing the data to be sent to the hook via its stdin stream. None if no data needs to be sent. hook_args: An iterable of command-line arguments to pass to the hook. None if no arguments are needed. cwd: The working directory from which to execute the hook. If None, the hook is executed from the current working directory. RETURN VALUE Return a tuple with the following elements: - The name of the script called as a hook; - The Popen object corresponding the script's execution (which, by the time this function returns, has finished executing); - The output of the script (stdout + stderr combined). """ hook_cmd = [self.hook_exe] if hook_args is not None: hook_cmd.extend(hook_args) try: p = Popen( hook_cmd, stdin=PIPE if hook_input is not None else None, stdout=PIPE, stderr=STDOUT, cwd=cwd, ) except OSError as E: raise InvalidUpdate("Invalid {self.hook_option_name} configuration" " ({self.hook_exe}):\n" "{err_info}".format(self=self, err_info=str(E))) if hook_input is not None: hook_input = encode_utf8(hook_input) out, _ = p.communicate(hook_input) return (self.hook_exe, p, safe_decode(out))
def get_emails_from_script(script_filename, ref_name, changed_files): """The list of emails addresses for the given list of changed files. This list is obtained by running the given script, and passing it the list of changed files via stdin (one file per line). By convention, passing nothing via stdin (no file changed) should trigger the script to return all email addresses. PARAMETERS ref_name: The name of the reference being updated. script_filename: The name of the script to execute. changed_files: A list of files to pass to the script (via stdin). None is also accepted in place of an empty list. """ input_str = "" if changed_files is None else "\n".join(changed_files) p = Popen([script_filename, ref_name], stdin=PIPE, stdout=PIPE) (output, _) = p.communicate(input=encode_utf8(input_str)) if p.returncode != 0: warn("!!! %s failed with error code: %d." % (script_filename, p.returncode)) return safe_decode(output).splitlines()
def sendmail(from_email, to_emails, mail_as_string, smtp_server): """Send an email with sendmail. PARAMETERS from_email: the address sending this email (e.g. [email protected]) to_emails: A list of addresses to send this email to. mail_as_string: the message to send (with headers) RETURNS A boolean (sent / not sent) REMARKS We prefer running sendmail over using smtplib because sendmail queues the email and retries a few times if the target server is unable to receive the email. """ sendmail = get_sendmail_exe() p = Popen([sendmail] + to_emails, stdin=PIPE, stdout=PIPE, stderr=STDOUT) out, _ = p.communicate(encode_utf8(mail_as_string)) if p.returncode != 0 or "GIT_HOOKS_TESTSUITE_MODE" in os.environ: print(safe_decode(out)) return p.returncode == 0
def __call_filer_cmd(self): """Call self.filer_cmd to get self.email_body filed. The contents that gets filed is a slightly augmented version of self.email to provide a little context of what's being changed. Prints a message on stdout in case of error returned during the call. """ ref_name = self.ref_name if ref_name.startswith("refs/heads/"): # Replace the reference name by something a little more # intelligible for normal users. ref_name = "The %s branch" % ref_name[11:] to_be_filed = ("%s has been updated by %s:" % (ref_name, self.email_info.email_from) + "\n\n" + self.email_body) p = Popen(self.filer_cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) out, _ = p.communicate(encode_utf8(to_be_filed)) if p.returncode != 0: print(safe_decode(out))
def git_attribute(commit_rev, filename_list, attr_name): """Return filename's attribute value at commit_rev. PARAMETERS commit_rev: The commit to use in order to determine the attribute value. This is important, because more recent commits may have changed the attribute value through updates of various .gitattributes files. filename_list: A list of filenames for which the attribute is to be determined. The file name should be relative to the root of the repository. attr_name: The name of the attribute. RETURN VALUE A dictionary, where the key is a the filename (one key for each file in filename_list), and the value is the file's attribute value as returned by git (Eg. 'set', 'unset', 'unspecified', etc). REMARKS The problem is not as easy as it looks. If we were working from a full (non-bare) repository, the `git check-attr' command would give us our answer immediately. But in bare repositories, the only file read is GIT_DIR/info/attributes. Originally, we implemented this way: Starting from the directory where our file is located, find the first .gitattribute file that specifies an attribute value for our file. Unfortunately, reading the gitattributes(5) man page more careful, we realized that this does not implement gitattributes semantics properly (we don't stop once we found a .gitattributes file with an entry that matches). Also, this approach turned out to be extremely slow, and could cause some updates to take minutes to process for commits where 2-3 thousand files were modified (typical when updating the copyright year, for instance). So, instead of trying to re-implement the git-check-attr command ourselves, what we do now, is create a dummy git repository inside which we (lazily) reproduce the directory tree, with their .gitattributes file. And then, from there call `git check-attr'. And, to help with the performance aspect, we call it only once requesting the attribute value for all files all in one go. """ # Verify that we have a scratch area we can use for create the fake # git repository (see REMARKS section above). assert utils.scratch_dir is not None # A copy of the environment, but without the GIT_DIR environment # variable (which gets sets when called by git), pointing to # the repository to which changes are being pushed. This interferes # with most git commands when we're trying to work with our fake # repository. So we use this copy of the environment without # the GIT_DIR environment variable when needed. tmp_git_dir_env = dict(os.environ) tmp_git_dir_env.pop("GIT_DIR", None) tmp_git_dir = mkdtemp(".git", "check-attr-", utils.scratch_dir) git.init(_cwd=tmp_git_dir, _env=tmp_git_dir_env) # There is one extra complication: We want to also provide support # for a DEFAULT_ATTRIBUTES_FILE, where the semantics is that, # if none of the .gitattributes file have an entry matching # our file, then this file is consulted. Once again, to avoid # calling `git check-attr' multiple times, what we do instead # is that we create a the directory tree in a root which is in # a subdir of tmp_git_dir. That way, we can put the default # attribute file in the root of tmp_git_dir, and git-check-attr # will only look at it if checked-in .gitattributes don't define # the attribute of a given file, thus implementing the "default" # behavior. # # This requires a bit of manipulation, because now, in the fake # git repository, the files we want to check are conceptually # inside the subdir. So filenames passed to `git check-attr' # have to contain that subdir, and the that subdir needs to be # excised from the command's output. if isfile(DEFAULT_ATTRIBUTES_FILE): copy(DEFAULT_ATTRIBUTES_FILE, os.path.join(tmp_git_dir, ".gitattributes")) checkout_subdir = "src" tmp_checkout_dir = os.path.join(tmp_git_dir, checkout_subdir) dirs_with_changes = {} for filename in filename_list: assert not os.path.isabs(filename) dir_path = filename dir_created = False while dir_path: dir_path = os.path.dirname(dir_path) if dir_path in dirs_with_changes: continue gitattributes_rel_file = os.path.join(dir_path, ".gitattributes") if cached_file_exists(commit_rev, gitattributes_rel_file): if not dir_created: os.makedirs(os.path.join(tmp_checkout_dir, dir_path)) dir_created = True git.show( "%s:%s" % (commit_rev, gitattributes_rel_file), _outfile=os.path.join(tmp_checkout_dir, gitattributes_rel_file), ) dirs_with_changes[dir_path] = True # To avoid having to deal with the parsing of quoted filenames, # we use the -z option of "git check-attr". What this does is # that each of the 3 elements of each line is now separated by # a NUL character. Also, each line now ends with a NUL character # as well, instead of LF. # # To parse the output, we split it at each NUL character. # This means that the output gets split into a sequence of # lines which go 3 by 3, with the first line containing # the filename, the second being the name of the attribute # being queried, and the third being the attribute's value # for that file. check_attr_input = "\x00".join( ["%s/%s" % (checkout_subdir, filename) for filename in filename_list]) attr_info = git.check_attr( "-z", "--stdin", attr_name, _cwd=tmp_git_dir, _env=tmp_git_dir_env, _input=encode_utf8(check_attr_input), _decode=True, ).split("\x00") if len(attr_info) % 3 == 1 and not attr_info[-1]: # The attribute information for each filename ends with # a NUL character, so the terminating NUL character in # the last entry caused the split to add one empty element # at the end. This is expected, so just remove it. attr_info.pop() # As per the above, we should now have a number of lines that's # a multiple of 3. assert len(attr_info) % 3 == 0 result = {} while attr_info: filename = attr_info.pop(0) attr_info.pop(0) # Ignore the attribute name... attr_val = attr_info.pop(0) assert filename.startswith(checkout_subdir + "/") filename = filename[len(checkout_subdir) + 1:] result[filename] = attr_val return result