<commit_before>#! /usr/bin/env python2
import argparse
from collections import namedtuple
import os.path
import logging
import sys

from sh import git, ErrorReturnCode_1

logging.basicConfig(format='%(levelname)s: %(message)s')

def make_enum(name, values):
    return namedtuple(name, values)._make(values)

Action = make_enum('Action', ('fetch', 'commit', 'pull', 'push'))
TreeState = make_enum('TreeState', ('clean', 'dirty'))
HeadState = make_enum('HeadState', ('up_to_date', 'older', 'newer', 'forked'))

def fail(msg):
    'Fail program with printed message'
    logging.error(msg)
    sys.exit(1)

def goto(repo_path):
    'find repo and change directory'
    repo = os.path.expanduser(repo_path)
    logging.debug('going to %s' % repo)
    if not os.path.isdir(repo):
        fail('No directory at %s!' % repo)
    if not os.path.isdir(os.path.join(repo, '.git')):
        fail('No git repo at %s!' % repo)
    os.chdir(repo)

def act(action):
    'perform given git action (with side effects)'
    assert action in Action
    if action == Action.fetch:
        git.fetch()
    elif action == Action.commit:
        git.add('.', all=True)
        msg = 'auto-commit by gitthemall'
        git.commit(message=msg)
    elif action == Action.pull:
        git.pull(rebase=True)
    else:
        raise NotImplementedError()

def get_tree_state():
    'parse git status and return current tree state'
    for line in git.status(porcelain=True):
        if line.strip():
            return TreeState.dirty
    return TreeState.clean

def is_ancestor(parent, child):
    try:
        git('merge-base', parent, child, is_ancestor=True)
        return True
    except ErrorReturnCode_1:
        return False

def get_head_state():
    local = 'HEAD'
    remote = '@{upstream}'
    if is_ancestor(remote, local):
        if is_ancestor(local, remote):
            return HeadState.up_to_date
        else:
            return HeadState.newer
    else:
        if is_ancestor(local, remote):
            return HeadState.older
        else:
            return HeadState.forked

def update(repo, actions):
    'Update repo according to allowed actions.'
    goto(repo)
    act(Action.fetch)

    # maybe commit?
    if get_tree_state() == TreeState.dirty:
        if Action.commit in actions:
            act(Action.commit)
        else:
            logging.info('Skip repo with dirty tree:')
            for line in git.status(porcelain=True):
                logging.info(line.rstrip())
            return
    assert get_tree_state() == TreeState.clean()

    # maybe pull?
    if get_head_state() in [HeadState.older, HeadState.forked]:
        if Action.pull in actions:
            act(Action.pull)
        else:
            logging.info('Skip repo with HEAD behind.')
            return

    # maybe push?<commit_msg>fix invalid use of parens<commit_after>#! /usr/bin/env python2
import argparse
from collections import namedtuple
import os.path
import logging
import sys

from sh import git, ErrorReturnCode_1

logging.basicConfig(format='%(levelname)s: %(message)s')

def make_enum(name, values):
    return namedtuple(name, values)._make(values)

Action = make_enum('Action', ('fetch', 'commit', 'pull', 'push'))
TreeState = make_enum('TreeState', ('clean', 'dirty'))
HeadState = make_enum('HeadState', ('up_to_date', 'older', 'newer', 'forked'))

def fail(msg):
    'Fail program with printed message'
    logging.error(msg)
    sys.exit(1)

def goto(repo_path):
    'find repo and change directory'
    repo = os.path.expanduser(repo_path)
    logging.debug('going to %s' % repo)
    if not os.path.isdir(repo):
        fail('No directory at %s!' % repo)
    if not os.path.isdir(os.path.join(repo, '.git')):
        fail('No git repo at %s!' % repo)
    os.chdir(repo)

def act(action):
    'perform given git action (with side effects)'
    assert action in Action
    if action == Action.fetch:
        git.fetch()
    elif action == Action.commit:
        git.add('.', all=True)
        msg = 'auto-commit by gitthemall'
        git.commit(message=msg)
    elif action == Action.pull:
        git.pull(rebase=True)
    else:
        raise NotImplementedError()

def get_tree_state():
    'parse git status and return current tree state'
    for line in git.status(porcelain=True):
        if line.strip():
            return TreeState.dirty
    return TreeState.clean

def is_ancestor(parent, child):
    try:
        git('merge-base', parent, child, is_ancestor=True)
        return True
    except ErrorReturnCode_1:
        return False

def get_head_state():
    local = 'HEAD'
    remote = '@{upstream}'
    if is_ancestor(remote, local):
        if is_ancestor(local, remote):
            return HeadState.up_to_date
        else:
            return HeadState.newer
    else:
        if is_ancestor(local, remote):
            return HeadState.older
        else:
            return HeadState.forked

def update(repo, actions):
    'Update repo according to allowed actions.'
    goto(repo)
    act(Action.fetch)

    # maybe commit?
    if get_tree_state() == TreeState.dirty:
        if Action.commit in actions:
            act(Action.commit)
        else:
            logging.info('Skip repo with dirty tree:')
            for line in git.status(porcelain=True):
                logging.info(line.rstrip())
            return
    assert get_tree_state() == TreeState.clean

    # maybe pull?
    if get_head_state() in [HeadState.older, HeadState.forked]:
        if Action.pull in actions:
            act(Action.pull)
        else:
            logging.info('Skip repo with HEAD behind.')
            return

    # maybe push?
{'fraction_non_alphanumeric': 0.11579320113314448, 'fraction_numerical': 0.00141643059490085, 'mean_word_length': 2.628131021194605, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2826301', 'n_tokens_mistral': 1737, 'n_tokens_neox': 1718, 'n_words': 475}