summaryrefslogtreecommitdiffstats
path: root/tools/git_commit.py
blob: cc4061f25582afdb382c9c534ca31bc715f472cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python
""" Trigger script to commit selected changes to a local repository
back to git.  To use this script, enable the Trigger plugin, put this
script in /var/lib/bcfg2/Trigger/, and create /etc/bcfg2-commit.conf.

The config file, /etc/bcfg2-commit.conf, may contain four options in
the [global] section:

* "config" is the path to the Bcfg2 server config file.  (Default:
  /etc/bcfg2.conf)
* "commit" is a comma-separated list of globs giving the paths that
  should be committed back to the repository.  Default is 'SSLCA/*,
  SSHbase/*, Cfg/*', which will commit data back for SSLCA, SSHbase,
  Cfg, FileProbes, etc., but not, for instance, Probes/probed.xml.
  You may wish to add Metadata/clients.xml to the commit list.
* "debug" and "verbose" let you set the log level for git_commit.py
  itself.
"""


import os
import sys
import git
import logging
import Bcfg2.Logger
import Bcfg2.Options
from Bcfg2.Compat import ConfigParser
from fnmatch import fnmatch

# config file path
CONFIG = "/etc/bcfg2-commit.conf"

# config defaults.  all config options are in the [global] section
DEFAULTS = dict(config='/etc/bcfg2.conf',
                commit="SSLCA/*, SSHbase/*, Cfg/*")


def list_changed_files(repo):
    return [d for d in repo.index.diff(None)
            if (d.a_blob is not None and not d.deleted_file and
                not d.renamed and not d.new_file)]


def add_to_commit(patterns, path, repo, relpath):
    progname = os.path.basename(sys.argv[0])
    logger = logging.getLogger(progname)
    for pattern in patterns:
        if fnmatch(path, os.path.join(relpath, pattern)):
            logger.debug("%s: Adding %s to commit" % (progname, path))
            repo.index.add([path])
            return True
    return False


def parse_options():
    config = ConfigParser.SafeConfigParser(DEFAULTS)
    config.read(CONFIG)

    optinfo = dict(
        profile=Bcfg2.Options.CLIENT_PROFILE,
        dryrun=Bcfg2.Options.CLIENT_DRYRUN,
        groups=Bcfg2.Options.Option("Groups",
                                    default=[],
                                    cmd="-g",
                                    odesc='<group>:<group>',
                                    cook=Bcfg2.Options.colon_split))
    optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
    optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
    argv = [Bcfg2.Options.CFILE.cmd, config.get("global", "config")]
    argv.extend(sys.argv[1:])
    setup = Bcfg2.Options.OptionParser(optinfo, argv=argv)
    setup.parse(argv)

    setup['commit'] = Bcfg2.Options.list_split(config.get("global",
                                                          "commit"))
    for opt in ['debug', 'verbose']:
        try:
            setup[opt] = config.getboolean("global", opt)
        except ConfigParser.NoOptionError:
            pass

    try:
        hostname = setup['args'][0]
    except IndexError:
        print(setup.hm)
        raise SystemExit(1)
    return (setup, hostname)


def setup_logging(setup):
    progname = os.path.basename(sys.argv[0])
    log_args = dict(to_syslog=setup['syslog'], to_console=sys.stdout.isatty(),
                    to_file=setup['logging'], level=logging.WARNING)
    if setup['debug']:
        log_args['level'] = logging.DEBUG
    elif setup['verbose']:
        log_args['level'] = logging.INFO
    Bcfg2.Logger.setup_logging(progname, **log_args)
    return logging.getLogger(progname)


def main():
    progname = os.path.basename(sys.argv[0])
    setup, hostname = parse_options()
    logger = setup_logging(setup)
    if setup['dryrun']:
        logger.info("%s: In dry-run mode, changes will not be committed" %
                    progname)

    if setup['vcs_root']:
        gitroot = os.path.realpath(setup['vcs_root'])
    else:
        gitroot = os.path.realpath(setup['repo'])
    logger.info("%s: Using Git repo at %s" % (progname, gitroot))
    try:
        repo = git.Repo(gitroot)
    except:  # pylint: disable=W0702
        logger.error("%s: Error setting up Git repo at %s: %s" %
                     (progname, gitroot, sys.exc_info()[1]))
        return 1

    # canonicalize the repo path so that git will recognize it as
    # being inside the git repo
    bcfg2root = os.path.realpath(setup['repo'])

    if not bcfg2root.startswith(gitroot):
        logger.error("%s: Bcfg2 repo %s is not inside Git repo %s" %
                     (progname, bcfg2root, gitroot))
        return 1

    # relative path to Bcfg2 root from VCS root
    if gitroot == bcfg2root:
        relpath = ''
    else:
        relpath = bcfg2root[len(gitroot) + 1:]

    new = 0
    changed = 0
    logger.debug("%s: Untracked files: %s" % (progname, repo.untracked_files))
    for path in repo.untracked_files:
        if add_to_commit(setup['commit'], path, repo, relpath):
            new += 1
        else:
            logger.debug("%s: Not adding %s to commit" % (progname, path))
    logger.debug("%s: Untracked files after building commit: %s" %
                 (progname, repo.untracked_files))

    changes = list_changed_files(repo)
    logger.info("%s: Changed files: %s" % (progname,
                                           [d.a_blob.path for d in changes]))
    for diff in changes:
        if add_to_commit(setup['commit'], diff.a_blob.path, repo, relpath):
            changed += 1
        else:
            logger.debug("%s: Not adding %s to commit" % (progname,
                                                          diff.a_blob.path))
    logger.info("%s: Changed files after building commit: %s" %
                (progname, [d.a_blob.path for d in list_changed_files(repo)]))

    if new + changed > 0:
        logger.debug("%s: Committing %s new files and %s changed files" %
                     (progname, new, changed))
        if setup['dryrun']:
            logger.warning("%s: In dry-run mode, skipping commit and push" %
                           progname)
        else:
            output = repo.index.commit("Auto-commit with %s from %s run" %
                                       (progname, hostname))
            if output:
                logger.debug("%s: %s" % (progname, output))
            remote = repo.remote()
            logger.debug("%s: Pushing to remote %s at %s" % (progname, remote,
                                                             remote.url))
            output = remote.push()
            if output:
                logger.debug("%s: %s" % (progname, output))
    else:
        logger.info("%s: No changes to commit" % progname)

if __name__ == '__main__':
    sys.exit(main())