Esempio n. 1
0
    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))
Esempio n. 2
0
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()
Esempio n. 3
0
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
Esempio n. 4
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))
Esempio n. 5
0
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