Skip to content

Instantly share code, notes, and snippets.

@tyrcho
Last active March 28, 2023 16:16
Show Gist options
  • Select an option

  • Save tyrcho/3f15273497ff9d5b5a0b12c672094387 to your computer and use it in GitHub Desktop.

Select an option

Save tyrcho/3f15273497ff9d5b5a0b12c672094387 to your computer and use it in GitHub Desktop.

Revisions

  1. Michel Daviot revised this gist Mar 28, 2023. 1 changed file with 4 additions and 0 deletions.
    4 changes: 4 additions & 0 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -137,6 +137,10 @@ nurture_mr() {
    nurture_project() {
    project_name=$1
    project_id=$(get_project_id $PROJECTS_FILTER $project_name)
    if [ "$project_id" = "" ] ; then
    echo "no id found for project $project_name" >&2
    exit 1
    fi
    echo "" >&2
    echo "nurturing project $project_name (id $project_id)" >&2
    echo "==========================================" >&2
  2. Michel Daviot revised this gist Mar 10, 2022. 1 changed file with 0 additions and 4 deletions.
    4 changes: 0 additions & 4 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -112,8 +112,6 @@ all_open_mrs() {


    nurture_mr() {
    set -x

    local project_id=$1
    local mr_id=$2
    local project_name=$3
    @@ -134,8 +132,6 @@ nurture_mr() {
    merge_mr $project_id $mr_id true
    fi
    fi

    set +x
    }

    nurture_project() {
  3. Michel Daviot revised this gist Mar 10, 2022. 1 changed file with 5 additions and 1 deletion.
    6 changes: 5 additions & 1 deletion merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -112,6 +112,8 @@ all_open_mrs() {


    nurture_mr() {
    set -x

    local project_id=$1
    local mr_id=$2
    local project_name=$3
    @@ -122,7 +124,7 @@ nurture_mr() {
    merge_status=$(get_merge_status $project_id $mr_id)
    echo "merge status is $merge_status" >&2
    if [ "$pipeline_status" = "success" ] && [ "$merge_status" = "can_be_merged" ] ; then
    if [ ! merge_mr $project_id $mr_id ] ; then
    if ! merge_mr $project_id $mr_id ; then
    if rebase_local $project_id $mr_id ; then
    merge_mr $project_id $mr_id true
    fi
    @@ -132,6 +134,8 @@ nurture_mr() {
    merge_mr $project_id $mr_id true
    fi
    fi

    set +x
    }

    nurture_project() {
  4. Michel Daviot revised this gist Mar 10, 2022. 1 changed file with 15 additions and 8 deletions.
    23 changes: 15 additions & 8 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -31,7 +31,7 @@ merge_mr() {
    ret=$?
    echo
    if [ $ret -eq 0 ] ; then
    echo "MR $MR_ID was merged" >&2
    echo "MR $MR_ID was set to merge when pipeline succeeds" >&2
    else
    echo "MR $MR_ID was not merged" >&2
    fi
    @@ -76,7 +76,14 @@ EOF
    echo "* merge=subline" >> .git/info/attributes

    GIT_SUBLINE_MERGE_NON_INTERACTIVE_MODE=TRUE git rebase --verbose origin/$TARGET_BRANCH
    git push -f
    pushed=$(git push -f 2>&1 )
    if [ "$pushed" = "Everything up-to-date" ] ; then
    echo "No changes pushed to MR"
    return 1
    else
    sleep 10
    return 0
    fi
    }

    get_pipeline_status() {
    @@ -116,14 +123,14 @@ nurture_mr() {
    echo "merge status is $merge_status" >&2
    if [ "$pipeline_status" = "success" ] && [ "$merge_status" = "can_be_merged" ] ; then
    if [ ! merge_mr $project_id $mr_id ] ; then
    rebase_local $project_id $mr_id
    sleep 10
    merge_mr $project_id $mr_id true
    if rebase_local $project_id $mr_id ; then
    merge_mr $project_id $mr_id true
    fi
    fi
    else
    rebase_local $project_id $mr_id
    sleep 10
    merge_mr $project_id $mr_id true
    if rebase_local $project_id $mr_id ; then
    merge_mr $project_id $mr_id true
    fi
    fi
    }

  5. Michel Daviot revised this gist Sep 10, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion git-subline-merge
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    #!/usr/bin/env python
    #!/usr/bin/env python3

    #
    # git-subline-merge
  6. Michel Daviot revised this gist Sep 10, 2021. 1 changed file with 5 additions and 21 deletions.
    26 changes: 5 additions & 21 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -38,26 +38,6 @@ merge_mr() {
    return $ret
    }

    # Rebase the MR then sets it to "merge when pipeline succeeds"
    # not used anymore because gitlab fails to rebase when the merge is not trivial, replaced with rebase_local
    rebase_mr() {
    local PROJECT_ID=$1
    local MR_ID=$2
    local PROJECT_NAME=$3

    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_ID/rebase" -X PUT
    ret=$?
    if [ $ret -eq 0 ] ; then
    echo "MR $MR_ID was rebased"
    sleep 10
    merge_mr $PROJECT_ID $MR_ID true
    else
    echo "Gitlab could not rebase MR $MR_ID , trying locally"
    rebase_local $MR_ID
    fi
    return $ret
    }

    # clones the git repository locally and uses a subline merge algorithm to rebase and force push the branch
    rebase_local() {
    local PROJECT_ID=$1
    @@ -135,7 +115,11 @@ nurture_mr() {
    merge_status=$(get_merge_status $project_id $mr_id)
    echo "merge status is $merge_status" >&2
    if [ "$pipeline_status" = "success" ] && [ "$merge_status" = "can_be_merged" ] ; then
    merge_mr $project_id $mr_id
    if [ ! merge_mr $project_id $mr_id ] ; then
    rebase_local $project_id $mr_id
    sleep 10
    merge_mr $project_id $mr_id true
    fi
    else
    rebase_local $project_id $mr_id
    sleep 10
  7. Michel Daviot revised this gist Jun 14, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -118,7 +118,7 @@ all_open_mrs() {
    PROJECT_ID=$2

    curl_gitlab \
    "projects/$PROJECT_ID/merge_requests?state=opened&scope=all&author_id=$AUTHOR_ID" \
    "projects/$PROJECT_ID/merge_requests?state=opened&scope=all&author_id=$AUTHOR_ID&sort=asc" \
    | jq -r '.[] | "\(.iid)"' \
    | tr '\r\n' ' '
    }
  8. @colisweb-gitlab colisweb-gitlab revised this gist Apr 9, 2021. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -72,7 +72,7 @@ EOF

    read -r CI_PROJECT_PATH PROJECT_NAME << EOF
    $(curl_gitlab "projects/$PROJECT_ID" |
    jq -r '[.path_with_namespace, .name] | @tsv')
    jq -r '[.path_with_namespace, .path] | @tsv')
    EOF

    local GITLAB_USER_NAME="gitlab.ci1"
    @@ -85,6 +85,7 @@ EOF
    cd $PROJECT_NAME
    git checkout $SOURCE_BRANCH

    git config advice.detachedHead false
    git config user.email $GITLAB_USER_EMAIL
    git config user.name "$GITLAB_USER_NAME"

  9. @colisweb-gitlab colisweb-gitlab revised this gist Apr 9, 2021. 2 changed files with 835 additions and 7 deletions.
    779 changes: 779 additions & 0 deletions git-subline-merge
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,779 @@
    #!/usr/bin/env python

    #
    # git-subline-merge
    #
    # Created by Paul Altin on 02.03.18.
    # Downloaded from https://github.com/paulaltin/git-subline-merge

    #
    # An interactive git merge driver which can resolve non-overlapping conflicts on individual or adjacent lines.
    #
    # To install for use during merge/rebase, place this script somewhere on your path, add these lines to your ~/.gitconfig:
    #
    # [merge "git-subline-merge"]
    # name = An interactive merge driver for resolving sub-line conflicts
    # driver = git-subline-merge %O %A %B %L %P
    # recursive = binary
    #
    # and this to your git attributes file (e.g. ~/.config/git/attributes):
    #
    # * merge=git-subline-merge
    #
    # Alternatively, run this script on a conflicted file using 'git-subline-merge /path/to/file'
    #


    import os, sys, re, tempfile
    from subprocess import call
    from shutil import copyfile
    from builtins import input

    # improved command line editing on *nix
    try:
    import readline
    except ImportError:
    pass

    # for interpreting environment variables
    try:
    from distutils.util import strtobool
    except ImportError:
    from distutils import strtobool


    #############
    ### SETUP ###
    #############

    WINDOWS = os.name == 'nt'

    # max hunk sizes
    MAX_HUNK_SIZE = int(os.getenv('GIT_SUBLINE_MERGE_MAX_HUNK_SIZE', 16))
    MAX_HUNK_SIZE_DIFF = int(os.getenv('GIT_SUBLINE_MERGE_MAX_HUNK_SIZE_DIFF', 8))

    # colors
    # don't use color if stdout is not a terminal or if it doesn't support at least 8-bit color
    # also respect NO_COLOR informal standard
    stdout_isatty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
    term_colors = int(os.popen('which tput >/dev/null && tput colors').read() or 0)
    no_color = os.getenv('NO_COLOR') is not None
    show_color = stdout_isatty and term_colors >= 8 and not no_color

    # use colorama if available for colored terminal output on Windows
    if WINDOWS and show_color:
    try:
    from colorama import init
    init()
    except ImportError:
    print('Install colorama (pip install colorama) for colored output on Windows.')
    pass

    # interactive or non-interactive mode
    stdin_isatty = hasattr(sys.stdin, 'isatty') and sys.stdin.isatty()
    interactive = not bool(strtobool(os.getenv('GIT_SUBLINE_MERGE_NON_INTERACTIVE_MODE', 'False')))

    if interactive and not stdin_isatty and not WINDOWS:
    try:
    sys.stdin = open('/dev/tty')
    except IOError:
    print('Warning: git-subline-merge could not run in interactive mode because there is no controlling terminal.\n'
    'You can enable non-interactive mode using the GIT_SUBLINE_MERGE_NON_INTERACTIVE_MODE environment variable,\n'
    ' however note that in this mode you will NOT be able to review merges before they are written to file.')
    sys.exit(1)



    ###############
    ### HELPERS ###
    ###############

    # for colored output in terminal
    class color:

    bold = '\033[1m' if show_color else ''
    welcome = '\033[1m\033[91m' if show_color else '' # bold, red
    info = '\033[96m' if show_color else '' # cyan
    highlight = '\033[103m\033[1m\033[30m' if show_color else '' # bold, black, yellow bg
    added1 = '\033[48;5;28m' if show_color else '' # bright green
    added2 = '\033[48;5;22m' if show_color else '' # dark green
    deleted1 = '\033[48;5;124m' if show_color else '' # bright red
    deleted2 = '\033[48;5;88m' if show_color else '' # dark red
    deleted_both = '\033[48;5;166m' if show_color else '' # orange
    success = '\033[92m' if show_color else '' # green
    warning = '\033[93m' if show_color else '' # yellow
    error = '\033[91m' if show_color else '' # red
    end = '\033[0m' if show_color else ''


    # get number of lines in a file, either from a name or a file handle
    # if given a handle, ensure position is reset to its original value
    def file_len(f):
    i = 0
    # file
    try:
    with open(f, 'r') as fh:
    for i,l in enumerate(fh):
    pass
    # file handle
    except TypeError:
    pos = f.tell()
    f.seek(0)
    for i,l in enumerate(f):
    pass
    f.seek(pos)

    return i + 1


    # get the line numbers of the beginning and end of a conflicted hunk
    # the file parameter is a path to a file on disk
    def find_nth_conflicted_hunk(file, n):
    start = stop = -1
    with open(file, 'r') as f:
    m = 0
    for i,l in enumerate(f):
    if l.startswith(marker_start):
    m += 1
    if (m == n and start == -1):
    start = i
    elif start >= 0 and l.startswith(marker_end):
    stop = i + 1
    break
    if start == -1 or stop == -1:
    raise IndexError('Hunk %d hunk not found!' % n)
    return (start, stop)


    # find nth conflict in file and replace it with given lines
    # the file parameter is a path to a file on disk
    # lines is a list of strings
    def replace_nth_conflicted_hunk_with_lines(file, n, lines):
    contents = None
    with open(file, 'r') as f:
    contents = f.readlines()
    start, stop = find_nth_conflicted_hunk(file, n)
    contents[start:stop] = lines
    with open(file, 'w') as f:
    f.write(''.join(contents))


    # prompt user for input, repeating until one of the allowed responses is given
    def ask_for_input(msg, allowed=None):

    if allowed is not None:
    msg = color.bold + color.info + msg + ' (' + '/'.join(allowed) + ')? ' + color.end
    while True:
    r = input(msg).strip().lower()
    if r in [s.strip().lower() for s in allowed]:
    break
    else:
    msg = color.bold + color.info + msg + color.end
    r = input(msg)

    return r

    # get exit status from a call to os.system
    # on Windows, os.WEXITSTATUS() doesn't work and os.system() returns the status directly
    def get_exit_status(exit_code):
    return exit_code if WINDOWS else os.WEXITSTATUS(exit_code)



    #############
    ### DIFFS ###
    #############

    # print one version of a hunk, highlighting the differences between it and the other version(s)
    # display is either 'current', 'base' or 'other'
    # added lines are indicated by a leading '+' and are highlighted in green
    # deleted lines are indicated by a leading '-' and are highlighted in red
    # changed lines are indicated by a leading '*' and changes within them are highlighted in either green or red
    # for current or other version, we just need to highlight the differences between it
    # and the base version, which can only be changes or additions
    # for the base version, we want to highlight the differences between it and BOTH
    # the current and the other version, which can only be changes or deletions
    # we take the diff with both versions and combine them by keeping all color codes
    def print_formatted_diff(hunk, display):

    if display == 'current' or display == 'other':

    old = hunk[1]
    new = hunk[0] if display == 'current' else hunk[2]
    color_added = color.added1 if display == 'current' else color.added2
    color_deleted = color.deleted1 if display == 'current' else color.deleted2
    clines = format_diff(old, new, 'new', color_added, color_deleted)

    for ln in clines:
    print(ln)

    elif display == 'base':

    clines1 = format_diff(hunk[1], hunk[0], 'old', color.added1, color.deleted1)
    clines2 = format_diff(hunk[1], hunk[2], 'old', color.added2, color.deleted2)
    clines = combine_colors(clines1, clines2, color.deleted1, color.deleted2, color.deleted_both)

    for ln in clines:
    print(ln)


    # add colors and prefixes showing the parts added or deleted between two versions
    # fold and fnew are file handles for the old and new versions
    # display must be either 'old' or 'new'
    # added lines are indicated by a leading '+' and are highlighted using color_added
    # deleted lines are indicated by a leading '-' and are highlighted using color_deleted
    # changed lines are indicated by a leading '*' and changes within them are highlighted using color_added and color_deleted
    def format_diff(fold, fnew, display, color_added, color_deleted):

    # reset file handles
    fold.seek(0)
    fnew.seek(0)

    # find line numbers of added, deleted and changed lines
    # for added and deleted groups, get the (zero-indexed) number of the first line and the length of the group
    # for changed groups, get the (zero-indexed) numbers of the first and one past the last line
    # ideally we'd use \n as the delimiter, but these break the shell on Windows
    delimiter = marker_start + "GIT_SUBLINE_MERGE_DELIMITER" + marker_end
    oldformat = ' --old-group-format="D%de+%dn,%dE+%dN' + delimiter + '" '
    newformat = ' --new-group-format="A%de+%dn,%dE+%dN' + delimiter + '" '
    changedformat = ' --changed-group-format="C%de+%dl,%dE+%dL' + delimiter + '" '
    unchangedformat = ' --unchanged-group-format="" '
    cmd = 'diff' + oldformat + newformat + changedformat + unchangedformat + fold.name + ' ' + fnew.name
    hunks = [h for h in os.popen(cmd).read().split(delimiter) if len(h)]

    # extract indices from diff output
    # added and deleted are a list of line numbers
    # changed are lists of (start, stop) pairs
    added, deleted, oldchange, newchange = [], [], [], []

    for h in hunks:
    if h[0] == 'A':
    a = h[1:].split(',')[1]
    start,length = [int(x) for x in a.split('+')]
    added += list(range(start, start+length))
    elif h[0] == 'D':
    d = h[1:].split(',')[0]
    start,length = [int(x) for x in d.split('+')]
    deleted += list(range(start, start+length))
    elif h[0] == 'C':
    c = h[1:].split(',')
    start,stop = [int(x) for x in c[0].split('+')]
    oldchange += [[start, stop]]
    start,stop = [int(x) for x in c[1].split('+')]
    newchange += [[start, stop]]

    # generate a list of lines with prefixes and highlighting
    lines = []
    i, j = 0, 0
    old = fold.read().splitlines()
    new = fnew.read().splitlines()
    version = old if display == 'old' else new
    change = oldchange if display == 'old' else newchange

    while i < len(version):
    if display == 'new' and i in added:
    lines.append('+ ' + color_added + version[i] + color.end)
    elif display == 'old' and i in deleted:
    lines.append('- ' + color_deleted + version[i] + color.end)
    elif j < len(change) and i == change[j][0]:
    oldhunk = old[oldchange[j][0]:oldchange[j][1]]
    newhunk = new[newchange[j][0]:newchange[j][1]]
    coloredhunk = format_changed_hunk(oldhunk, newhunk, display, color_added, color_deleted)
    for ln in coloredhunk:
    lines.append('* ' + ln)
    i += change[j][1] - change[j][0] - 1 # 1 will be added later
    j += 1
    else:
    lines.append(' ' + version[i])
    i += 1

    return lines


    # word-level diff with coloring
    # additions are highlighted using color_added, deletions using color_deleted
    # h1 and h2 are lists of strings (lines)
    # display must be either 'old' or 'new'
    # return value is a list of strings (lines) that matches
    # either h1 or h2, depending on the value of display
    def format_changed_hunk(h1, h2, display, color_added, color_deleted):

    result = ''

    # split lines at non-word characters
    # keep track of where the linebreaks are in terms of segment index
    splA, splB, lcA, lcB = [], [], [0], [0]
    for ln in h1:
    segments = re.split('(\W)', ln)
    splA += segments
    lcA.append(lcA[-1] + len(segments))
    for ln in h2:
    segments = re.split('(\W)', ln)
    splB += segments
    lcB.append(lcB[-1] + len(segments))
    lcA.pop(0)
    lcB.pop(0)

    # write split lines to temp files
    tmpA = tempfile.NamedTemporaryFile(mode='w+')
    tmpA.write('\n'.join(splA))
    tmpA.flush()
    tmpB = tempfile.NamedTemporaryFile(mode='w+')
    tmpB.write('\n'.join(splB))
    tmpB.flush()

    # use diff to find added and removed section indices
    old = os.popen('diff --unchanged-line-format="." --old-line-format="-" --new-line-format="" ' + tmpA.name + ' ' + tmpB.name).read()
    new = os.popen('diff --unchanged-line-format="." --old-line-format="" --new-line-format="+" ' + tmpA.name + ' ' + tmpB.name).read()

    removed = []
    for i,c in enumerate(old):
    if c == '-':
    removed.append(i)
    added = []
    for i,c in enumerate(new):
    if c == '+':
    added.append(i)

    if display == 'old':
    for i,w in enumerate(splA):
    if i in lcA: result += '\n'
    if i in removed: result += color_deleted
    result += w.rstrip('\n')
    if i in removed: result += color.end

    elif display == 'new':
    for i,w in enumerate(splB):
    if i in lcB: result += '\n'
    if i in added: result += color_added
    result += w.rstrip('\n')
    if i in added: result += color.end

    tmpA.close()
    tmpB.close()

    return result.split('\n')


    # combine highlighting from two versions of the same (base) hunk and print
    # lines1 and lines2 are lists of strings (lines) and must be identical
    # except for highlighting with color1 and color2
    # regions which are highlighted in both versions will be printed with color_both
    def combine_colors(lines1, lines2, color1, color2, color_both):

    # merge the two versions
    lines = []
    assert len(lines1) == len(lines2)

    for ln1, ln2 in zip(lines1, lines2):

    i, j = 0, 0
    result = ''
    highlight1, highlight2 = False, False

    # remove prefixes (we will add them back later)
    prefixes = [ln1[0], ln2[0]]
    ln1, ln2 = ln1[2:], ln2[2:]

    # i iterates through ln1, j iterates through ln2
    while i < len(ln1) or j < len(ln2):

    # add special characters from version 1
    if i < len(ln1):

    if show_color and ln1[i:i+len(color1)] == color1:
    result += (color.end + color_both) if highlight2 else color1
    highlight1 = True
    i += len(color1)
    continue

    elif show_color and ln1[i:i+len(color.end)] == color.end:
    highlight1 = False
    result += color.end
    if highlight2: result += color2
    i += len(color.end)
    continue

    # add special characters from version 2
    if j < len(ln2):

    if show_color and ln2[j:j+len(color2)] == color2:
    result += (color.end + color_both) if highlight1 else color2
    highlight2 = True
    j += len(color2)
    continue

    elif show_color and ln2[j:j+len(color.end)] == color.end:
    highlight2 = False
    result += color.end
    if highlight1: result += color1
    j += len(color.end)
    continue

    # all other characters should match
    assert ln1[i] == ln2[j]
    result += ln1[i]
    i += 1
    j += 1

    # add prefix and print line
    prefix = '- ' if '-' in prefixes else '* ' if '*' in prefixes else ' '
    lines.append(prefix + result)

    return lines



    ###############
    ### MERGING ###
    ###############

    def process_hunk(hunk, index, resolved, num_conflicts):

    first_run = True
    fixed = abort = False

    # get number of lines in current, base and other hunks
    sizes = [file_len(f) for f in hunk]

    # only process small hunks
    if all([s <= MAX_HUNK_SIZE for s in sizes]) and max(abs(sizes[0]-sizes[1]), abs(sizes[2]-sizes[1])) <= MAX_HUNK_SIZE_DIFF:

    # interactive mode
    if interactive:

    print(color.highlight + '\nConflicted hunk %d of %d (spans %d/%d/%d lines) in %s...' % (index, num_conflicts, sizes[0], sizes[1], sizes[2], filename) + color.end)

    # ask user for action to take
    while True:

    for f in hunk: f.seek(0)

    if first_run:
    print('')
    action = 'v'
    first_run = False
    else:
    print('')
    print(color.bold + ' v - view entire hunk' + color.end)
    print(color.bold + ' x - view hunk in context' + color.end)
    print(color.bold + ' s - attempt sub-line merge' + color.end)
    print(color.bold + ' m - resolve manually' + color.end)
    print(color.bold + ' c - use current version' + color.end)
    print(color.bold + ' b - use base version' + color.end)
    print(color.bold + ' o - use other version' + color.end)
    print(color.bold + ' k - skip this hunk' + color.end)
    print(color.bold + ' q - skip all hunks in this file' + color.end)
    action = ask_for_input('Resolve this hunk', ['v','x','s','m','c','b','o','k','q'])
    print('')

    # actions
    if action == 'x':
    start, stop = find_nth_conflicted_hunk(current, index-resolved)
    with open(current, 'r') as c:
    for i,l in enumerate(c):
    if i in range(max(0, start-10), start):
    print(l.rstrip('\n'))
    elif i >= start:
    break
    if action in ['v','x']:
    print(color.info + marker_start + ' Current version' + color.end)
    print_formatted_diff(hunk, 'current')
    print(color.info + marker_base + ' Base version' + color.end)
    print_formatted_diff(hunk, 'base')
    print(color.info + marker_other + ' Other version' + color.end)
    print_formatted_diff(hunk, 'other')
    print(color.info + marker_end + color.end)
    if action == 'x':
    length = file_len(current)
    with open(current, 'r') as c:
    for i,l in enumerate(c):
    if i in range(stop, min(stop+10, length)):
    print(l.rstrip('\n'))
    elif i >= stop:
    break
    elif action == 's':
    result = subline_merge_hunk(hunk)
    if result is not None:
    print(color.info + marker_start + ' Sub-line merge yields:' + color.end)
    if len(result):
    print('\n'.join([x.rstrip('\n') for x in result]).rstrip('\n'))
    print(color.info + marker_end + color.end + '\n')
    accept = ask_for_input('Accept sub-line merge', ['y','n'])
    success = True if accept == 'y' else False
    if (success):
    replace_nth_conflicted_hunk_with_lines(current, index-resolved, result)
    fixed = True
    break
    else:
    print(color.bold + color.error + 'Sub-line merge failed, hunk has overlapping changes' + color.end)
    elif action == 'm':
    result = manual_merge_hunk(hunk)
    if result is not None:
    print(color.info + marker_start + ' Manual resolution is:' + color.end)
    if len(result):
    print('\n'.join([x.rstrip('\n') for x in result]).rstrip('\n'))
    print(color.info + marker_end + color.end + '\n')
    accept = ask_for_input('Accept manual resolution', ['y','n'])
    success = True if accept == 'y' else False
    if (success):
    replace_nth_conflicted_hunk_with_lines(current, index-resolved, result)
    fixed = True
    break
    else:
    print(color.bold + color.warning + 'User cancelled manual resolve' + color.end)
    elif action in ['c','b','o']:
    idx = ['c','b','o'].index(action)
    result = hunk[idx].readlines()
    branchname = ['Current','Base','Other'][idx]
    print(color.info + marker_start + ' ' + branchname + ' version is:' + color.end)
    if len(result):
    print('\n'.join([x.rstrip('\n') for x in result]).rstrip('\n'))
    print(color.info + marker_end + color.end + '\n')
    warn = ['the other branch', 'both branches', 'the current branch'][idx]
    print(color.bold + color.warning + 'Warning: this will discard changes on ' + warn + '!' + color.end + '\n')
    accept = ask_for_input('Accept', ['y','n'])
    success = True if accept == 'y' else False
    if (success):
    replace_nth_conflicted_hunk_with_lines(current, index-resolved, result)
    fixed = True
    break
    elif action == 'k':
    break
    elif action == 'q':
    abort = True
    break

    # non-interactive mode
    else:

    for f in hunk: f.seek(0)
    result = subline_merge_hunk(hunk)
    if result is not None:
    replace_nth_conflicted_hunk_with_lines(current, index-resolved, result)
    print(color.bold + color.success + 'git-subline-merge resolved conflict %d of %d in %s, resulting hunk was:' % (index, num_conflicts, filename) + color.end)
    print(color.info + marker_start + color.end)
    if len(result): print('\n'.join([x.rstrip('\n') for x in result]).rstrip('\n'))
    print(color.info + marker_end + color.end)
    fixed = True
    else:
    print(color.bold + color.error + 'git-subline-merge failed to resolve conflict %d of %d in %s, hunk has overlapping changes' % (index, num_conflicts, filename) + color.end)


    # hunk too large
    else:
    print(color.bold + color.info + '\nSkipping hunk %d of %d (spans %d/%d/%d lines) in %s, too large...' % (index, num_conflicts, sizes[0], sizes[1], sizes[2], filename) + color.end)

    return fixed, abort


    # attempt to do sub-line merging of a conflicted hunk
    def subline_merge_hunk(hunk):

    result = None
    fs = [None, None, None]

    # separate each character in a string by a newline character
    # on Windows the temp file can't be open twice, so we have to close it before calling
    # git-merge-file and delete it manually later
    for i in range(3):
    h = hunk[i].read()
    fs[i] = tempfile.NamedTemporaryFile(mode='w+', delete=False)
    fs[i].write('\n'.join(h[i:i+1] for i in range(len(h))))
    fs[i].close()

    # attempt merge on separated text
    status = os.system('git merge-file %s %s %s' % (fs[0].name, fs[1].name, fs[2].name))
    num_conflicts = get_exit_status(status)

    # if successful, take merge result after removing newlines
    # split into a list of lines
    if (num_conflicts == 0):
    with open(fs[0].name, 'r+') as f0:
    h = f0.read()
    result = h[0::2].splitlines(True)

    for f in fs: os.unlink(f.name)

    return result


    # open an editor for manual merging of a conflicted hunk
    def manual_merge_hunk(hunk):

    # prepare temp file for editing
    f = tempfile.NamedTemporaryFile(mode='w+', delete=False)
    f.write(marker_start + ' Current version is:\n')
    f.write(hunk[0].read())
    f.write(marker_base + ' Base version is:\n')
    f.write(hunk[1].read())
    f.write(marker_other + ' Other version is:\n')
    f.write(hunk[2].read())
    f.write(marker_end + '\n')
    f.close()

    # imitate the process that Git uses to determine which editor to use
    editor = os.getenv('GIT_EDITOR')
    if editor is None: editor = os.popen('git config core.editor').read().rstrip('\n') or None
    if editor is None: editor = os.getenv('VISUAL')
    if editor is None: editor = os.getenv('EDITOR')
    if editor is None: editor = 'vi'

    # known issue with vim on Mac OS X: can give nonzero exit status even when quit cleanly due to
    # errors in .vimrc, so check its normal exit code first.
    success_code = 0
    if editor == 'vi':
    success_code = call(['vi -c "q" %s' % f.name], shell=(not WINDOWS), stdin=sys.stdin)

    status = call(['%s %s' % (editor, f.name)], shell=(not WINDOWS), stdin=sys.stdin)

    # if editor returned without an error, return the saved lines
    result = None
    if status <= success_code:
    with open(f.name, 'r') as f:
    result = f.readlines()
    os.unlink(f.name)

    return result



    ############
    ### MAIN ###
    ############

    # invoked by git
    if len(sys.argv) == 6:

    # arguments passed in by git:
    # [1] name of temp file containing base version of file
    # [2] name of temp file containing current version of file
    # [3] name of temp file containing other version of file
    # [4] length of conflict markers (default is 7 for <<<<<<<)
    # [5] name of conflicted file
    # the result should be left in the current version (argv[2])
    base, current, other, marker_len, filename = sys.argv[1:]
    marker_len = int(marker_len)
    marker_start = '<' * marker_len
    marker_base = '|' * marker_len
    marker_other = '=' * marker_len
    marker_end = '>' * marker_len

    # run git merge on the files using diff3 (result is written to 'current')
    # the number of conflicts is encoded in the exit status of the git-merge-file command
    status = os.system('git merge-file --diff3 --marker-size=%d -L "Current version" -L "Base version" -L "Other version" %s %s %s' % (marker_len, current, base, other))
    num_conflicts = get_exit_status(status)

    # invoked manually on a single file
    elif len(sys.argv) >= 2 and os.path.isfile(sys.argv[1]):

    # first argument is path to file
    current = sys.argv[1]
    filename = current

    # optional second argument for conflict marker size (default is 7 for <<<<<<<)
    try:
    marker_len = int(sys.argv[2])
    except ValueError:
    print('Error: unable to read conflict marker size from argument "%s".' % (sys.argv[2]))
    sys.exit(1)
    except IndexError:
    pass
    finally:
    marker_len = 7;

    # extra arguments are ignored
    if len(sys.argv) > 3:
    print('Warning: ignoring extra arguments (%s).' % (', '.join(sys.argv[3:])))

    marker_start = '<' * marker_len
    marker_base = '|' * marker_len
    marker_other = '=' * marker_len
    marker_end = '>' * marker_len

    # use grep to find number of conflicts in the file
    num_start = int(os.popen('grep -c "^%s" "%s"' % (marker_start, current)).read())
    num_base = int(os.popen('grep -c "^%s" "%s"' % (marker_base, current)).read())
    num_other = int(os.popen('grep -c "^%s" "%s"' % (marker_other, current)).read())
    num_end = int(os.popen('grep -c "^%s" "%s"' % (marker_end, current)).read())

    # check that the number of conflict markers match
    # if they don't, probably the merge wasn't generated using diff3, and we can't resolve conflicts without the base version
    if num_start == num_base == num_other == num_end:
    num_conflicts = num_start
    else:
    print('Badly formatted conflicts found in file. Possibly you need to change your conflictstyle to diff3?')
    sys.exit(1)

    # otherwise show help text
    else:

    print("usage: git-subline-merge <file> [conflict-marker-size]\n"
    " <file>: path to file with conflicts (must be generated using diff3 style, see README)\n"
    " [conflict-marker-size] (optional): number of characters used in conflict markers (default 7)")
    sys.exit(1)


    # sometimes (e.g. during interactive rebase) this script is called even though
    # there are no conflicts - in that case just exit successfully here
    if num_conflicts == 0:
    sys.exit(0)

    # welcome message
    if interactive:
    print(color.welcome + '\ngit-subline-merge v1.0\n' + color.end)

    # make temporary files
    # one is a copy of the conflicted 'current', to iterate with
    # three are used to hold the different versions of each conflicted hunk
    # open with 'a' so we can append without worrying about newline characters
    tmp = tempfile.TemporaryFile(mode='w+')
    with open(current, 'r') as c: tmp.write(c.read())
    tmp.seek(0)
    hunk = [ tempfile.NamedTemporaryFile(mode='a+'), tempfile.NamedTemporaryFile(mode='a+'), tempfile.NamedTemporaryFile(mode='a+') ]

    # process file line by line
    resolved = zone = index = 0

    for line in tmp:

    # beginning of conflict zone
    if line.startswith(marker_start):
    zone = 1
    for f in hunk:
    f.truncate(0)
    f.seek(0)

    # base version of conflict
    elif line.startswith(marker_base):
    zone = 2

    # other version of conflict
    elif line.startswith(marker_other):
    zone = 3

    # end of conflict zone
    elif line.startswith(marker_end):
    zone = 0
    index += 1
    fixed, abort = process_hunk(hunk, index, resolved, num_conflicts)
    if fixed: resolved += 1
    elif abort: break

    # append line to appropriate hunk zone
    elif zone:
    hunk[zone-1].write(line)


    # close all temp files (will be automatically deleted)
    tmp.close()
    for h in hunk: h.close()

    # print resolution
    newline = '\n' if interactive else ''
    col = color.success if resolved == num_conflicts else color.warning if resolved > 0 else color.error
    print(col + color.bold + newline + 'Resolved %d of %d conflicts in %s' % (resolved, num_conflicts, filename) + newline + color.end)

    # only exit with success if all conflicts were resolved
    sys.exit(0 if resolved == num_conflicts else 1)

    63 changes: 56 additions & 7 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,7 @@
    #!/usr/bin/env bash

    script_full_path="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"

    curl_gitlab() {
    echo $* >&2
    URL_SUFFIX=$1
    @@ -37,9 +39,11 @@ merge_mr() {
    }

    # Rebase the MR then sets it to "merge when pipeline succeeds"
    # not used anymore because gitlab fails to rebase when the merge is not trivial, replaced with rebase_local
    rebase_mr() {
    PROJECT_ID=$1
    MR_ID=$2
    local PROJECT_ID=$1
    local MR_ID=$2
    local PROJECT_NAME=$3

    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_ID/rebase" -X PUT
    ret=$?
    @@ -48,11 +52,52 @@ rebase_mr() {
    sleep 10
    merge_mr $PROJECT_ID $MR_ID true
    else
    echo "MR $MR_ID was not rebased"
    echo "Gitlab could not rebase MR $MR_ID , trying locally"
    rebase_local $MR_ID
    fi
    return $ret
    }

    # clones the git repository locally and uses a subline merge algorithm to rebase and force push the branch
    rebase_local() {
    local PROJECT_ID=$1
    local MR_ID=$2

    read -r SOURCE_BRANCH TARGET_BRANCH << EOF
    $(curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_ID" |
    jq -r '[.source_branch, .target_branch] | @tsv')
    EOF

    echo "Try a local rebase for MR $MR_ID from $SOURCE_BRANCH to $TARGET_BRANCH"

    read -r CI_PROJECT_PATH PROJECT_NAME << EOF
    $(curl_gitlab "projects/$PROJECT_ID" |
    jq -r '[.path_with_namespace, .name] | @tsv')
    EOF

    local GITLAB_USER_NAME="gitlab.ci1"
    echo "Checkout https://${GITLAB_USER_NAME}@gitlab.com/${CI_PROJECT_PATH}.git in /tmp/$PROJECT_NAME"


    cd /tmp
    rm -rf $PROJECT_NAME
    git clone "https://${GITLAB_USER_NAME}:${GITLAB_PAT}@gitlab.com/${CI_PROJECT_PATH}.git"
    cd $PROJECT_NAME
    git checkout $SOURCE_BRANCH

    git config user.email $GITLAB_USER_EMAIL
    git config user.name "$GITLAB_USER_NAME"

    # setup subline merge https://github.com/paulaltin/git-subline-merge
    git config merge.conflictStyle diff3
    git config merge.subline.driver "$script_full_path/git-subline-merge %O %A %B %L %P"
    git config merge.subline.recursive binary
    echo "* merge=subline" >> .git/info/attributes

    GIT_SUBLINE_MERGE_NON_INTERACTIVE_MODE=TRUE git rebase --verbose origin/$TARGET_BRANCH
    git push -f
    }

    get_pipeline_status() {
    PROJECT_ID=$1
    MR_IID=$2
    @@ -79,8 +124,10 @@ all_open_mrs() {


    nurture_mr() {
    project_id=$1
    mr_id=$2
    local project_id=$1
    local mr_id=$2
    local project_name=$3

    echo "nurturing MR $mr_id" >&2
    pipeline_status=$(get_pipeline_status $project_id $mr_id)
    echo "pipeline status is $pipeline_status" >&2
    @@ -89,7 +136,9 @@ nurture_mr() {
    if [ "$pipeline_status" = "success" ] && [ "$merge_status" = "can_be_merged" ] ; then
    merge_mr $project_id $mr_id
    else
    rebase_mr $project_id $mr_id
    rebase_local $project_id $mr_id
    sleep 10
    merge_mr $project_id $mr_id true
    fi
    }

    @@ -104,7 +153,7 @@ nurture_project() {
    echo "MRs found : [$mrs]" >&2
    for mr_id in $mrs ; do
    ((nurtured_count < MAX_MRS)) || return 0
    if nurture_mr $project_id $mr_id; then
    if nurture_mr $project_id $mr_id $project_name; then
    ((nurtured_count++))
    fi
    done
  10. Michel Daviot revised this gist Apr 8, 2021. 1 changed file with 14 additions and 3 deletions.
    17 changes: 14 additions & 3 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -60,6 +60,13 @@ get_pipeline_status() {
    | jq -r ' "\(.pipeline.status)"'
    }

    get_merge_status() {
    PROJECT_ID=$1
    MR_IID=$2
    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_IID" \
    | jq -r ' "\(.merge_status)"'
    }

    all_open_mrs() {
    AUTHOR_ID=$1
    PROJECT_ID=$2
    @@ -75,9 +82,11 @@ nurture_mr() {
    project_id=$1
    mr_id=$2
    echo "nurturing MR $mr_id" >&2
    status=$(get_pipeline_status $project_id $mr_id)
    echo "pipeline status is $status" >&2
    if [ "$status" = "success" ] ; then
    pipeline_status=$(get_pipeline_status $project_id $mr_id)
    echo "pipeline status is $pipeline_status" >&2
    merge_status=$(get_merge_status $project_id $mr_id)
    echo "merge status is $merge_status" >&2
    if [ "$pipeline_status" = "success" ] && [ "$merge_status" = "can_be_merged" ] ; then
    merge_mr $project_id $mr_id
    else
    rebase_mr $project_id $mr_id
    @@ -87,7 +96,9 @@ nurture_mr() {
    nurture_project() {
    project_name=$1
    project_id=$(get_project_id $PROJECTS_FILTER $project_name)
    echo "" >&2
    echo "nurturing project $project_name (id $project_id)" >&2
    echo "==========================================" >&2
    local nurtured_count=0
    local mrs=$(all_open_mrs $AUTHOR_ID $project_id)
    echo "MRs found : [$mrs]" >&2
  11. Michel Daviot revised this gist Apr 7, 2021. 1 changed file with 14 additions and 7 deletions.
    21 changes: 14 additions & 7 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -66,7 +66,8 @@ all_open_mrs() {

    curl_gitlab \
    "projects/$PROJECT_ID/merge_requests?state=opened&scope=all&author_id=$AUTHOR_ID" \
    | jq -r '.[] | "\(.iid)"'
    | jq -r '.[] | "\(.iid)"' \
    | tr '\r\n' ' '
    }


    @@ -76,16 +77,21 @@ nurture_mr() {
    echo "nurturing MR $mr_id" >&2
    status=$(get_pipeline_status $project_id $mr_id)
    echo "pipeline status is $status" >&2
    [ "$status" = "success" ] && merge_mr $project_id $mr_id ||rebase_mr $project_id $mr_id
    if [ "$status" = "success" ] ; then
    merge_mr $project_id $mr_id
    else
    rebase_mr $project_id $mr_id
    fi
    }

    nurture_project() {
    project_name=$1
    project_id=$(get_project_id $PROJECTS_FILTER $project_name)
    echo "nurturing project $project_name (id $project_id)" >&2
    local nurtured_count=0

    for mr_id in $(all_open_mrs $AUTHOR_ID $project_id ) ; do
    local mrs=$(all_open_mrs $AUTHOR_ID $project_id)
    echo "MRs found : [$mrs]" >&2
    for mr_id in $mrs ; do
    ((nurtured_count < MAX_MRS)) || return 0
    if nurture_mr $project_id $mr_id; then
    ((nurtured_count++))
    @@ -98,20 +104,21 @@ nurture_project() {

    usage() {
    echo "Usage:"
    echo "$0 GITLAB_ACCESS_TOKEN AUTHOR_ID FILTER 'name1 name2 name3' <MAX_MRS>"
    echo "$0 GITLAB_ACCESS_TOKEN AUTHOR_ID FILTER 'name1 name2 name3' MAX_MRS"
    echo
    echo "examples:"
    echo "$0 df12fs1sdd 12345 'groups/789/search?scope=projects&search=' 'a b c' 3"
    echo "$0 df12fs1sdd 12345 'users/123/projects?search=' 'a b c'"
    exit 1
    }

    [ $# -gt 3 ] || usage
    [ $# -eq 5 ] || usage

    GITLAB_PAT=$1
    AUTHOR_ID=$2
    PROJECTS_FILTER=$3
    PROJECT_NAMES="$4"
    MAX_MRS=${8:-1}
    MAX_MRS=$5

    for project_name in $PROJECT_NAMES ; do
    nurture_project $project_name
  12. Michel Daviot revised this gist Mar 29, 2021. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -111,7 +111,7 @@ GITLAB_PAT=$1
    AUTHOR_ID=$2
    PROJECTS_FILTER=$3
    PROJECT_NAMES="$4"
    MAX_MRS=${10:-1}
    MAX_MRS=${8:-1}

    for project_name in $PROJECT_NAMES ; do
    nurture_project $project_name
  13. Michel Daviot revised this gist Mar 22, 2021. 1 changed file with 4 additions and 3 deletions.
    7 changes: 4 additions & 3 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -27,6 +27,7 @@ merge_mr() {

    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_ID/merge?should_remove_source_branch=true&merge_when_pipeline_succeeds=$MERGE_WHEN_PIPELINE_SUCCEEDS" -X PUT
    ret=$?
    echo
    if [ $ret -eq 0 ] ; then
    echo "MR $MR_ID was merged" >&2
    else
    @@ -35,7 +36,7 @@ merge_mr() {
    return $ret
    }

    # Rebases the MR then sets it to "merge when pipeline succeeds"
    # Rebase the MR then sets it to "merge when pipeline succeeds"
    rebase_mr() {
    PROJECT_ID=$1
    MR_ID=$2
    @@ -74,7 +75,7 @@ nurture_mr() {
    mr_id=$2
    echo "nurturing MR $mr_id" >&2
    status=$(get_pipeline_status $project_id $mr_id)
    echo "status is $status" >&2
    echo "pipeline status is $status" >&2
    [ "$status" = "success" ] && merge_mr $project_id $mr_id ||rebase_mr $project_id $mr_id
    }

    @@ -110,7 +111,7 @@ GITLAB_PAT=$1
    AUTHOR_ID=$2
    PROJECTS_FILTER=$3
    PROJECT_NAMES="$4"
    MAX_MRS=${5:-1}
    MAX_MRS=${10:-1}

    for project_name in $PROJECT_NAMES ; do
    nurture_project $project_name
  14. tyrcho revised this gist Apr 14, 2020. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -90,6 +90,9 @@ nurture_project() {
    ((nurtured_count++))
    fi
    done
    # finish with a return code 0 even if some MRs could not be merged / rebased
    echo "finished with project $project_name (id $project_id)" >&2
    echo "" >&2
    }

    usage() {
  15. tyrcho revised this gist Apr 6, 2020. 1 changed file with 0 additions and 0 deletions.
    Empty file modified merge_and_rebase.sh
    100644 → 100755
    Empty file.
  16. tyrcho revised this gist Apr 6, 2020. No changes.
  17. tyrcho created this gist Apr 6, 2020.
    114 changes: 114 additions & 0 deletions merge_and_rebase.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,114 @@
    #!/usr/bin/env bash

    curl_gitlab() {
    echo $* >&2
    URL_SUFFIX=$1
    shift

    curl \
    --silent \
    --show-error \
    --fail \
    --header "PRIVATE-TOKEN: $GITLAB_PAT" \
    "https://gitlab.com/api/v4/$URL_SUFFIX" $*
    }

    get_project_id() {
    PROJECTS_FILTER=$1
    NAME=$2
    curl_gitlab "$PROJECTS_FILTER$NAME" \
    | jq -r '.[] | select(.path == "'$NAME'") | "\(.id)"'
    }

    merge_mr() {
    PROJECT_ID=$1
    MR_ID=$2
    MERGE_WHEN_PIPELINE_SUCCEEDS=${3:-false}

    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_ID/merge?should_remove_source_branch=true&merge_when_pipeline_succeeds=$MERGE_WHEN_PIPELINE_SUCCEEDS" -X PUT
    ret=$?
    if [ $ret -eq 0 ] ; then
    echo "MR $MR_ID was merged" >&2
    else
    echo "MR $MR_ID was not merged" >&2
    fi
    return $ret
    }

    # Rebases the MR then sets it to "merge when pipeline succeeds"
    rebase_mr() {
    PROJECT_ID=$1
    MR_ID=$2

    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_ID/rebase" -X PUT
    ret=$?
    if [ $ret -eq 0 ] ; then
    echo "MR $MR_ID was rebased"
    sleep 10
    merge_mr $PROJECT_ID $MR_ID true
    else
    echo "MR $MR_ID was not rebased"
    fi
    return $ret
    }

    get_pipeline_status() {
    PROJECT_ID=$1
    MR_IID=$2
    curl_gitlab "projects/$PROJECT_ID/merge_requests/$MR_IID" \
    | jq -r ' "\(.pipeline.status)"'
    }

    all_open_mrs() {
    AUTHOR_ID=$1
    PROJECT_ID=$2

    curl_gitlab \
    "projects/$PROJECT_ID/merge_requests?state=opened&scope=all&author_id=$AUTHOR_ID" \
    | jq -r '.[] | "\(.iid)"'
    }


    nurture_mr() {
    project_id=$1
    mr_id=$2
    echo "nurturing MR $mr_id" >&2
    status=$(get_pipeline_status $project_id $mr_id)
    echo "status is $status" >&2
    [ "$status" = "success" ] && merge_mr $project_id $mr_id ||rebase_mr $project_id $mr_id
    }

    nurture_project() {
    project_name=$1
    project_id=$(get_project_id $PROJECTS_FILTER $project_name)
    echo "nurturing project $project_name (id $project_id)" >&2
    local nurtured_count=0

    for mr_id in $(all_open_mrs $AUTHOR_ID $project_id ) ; do
    ((nurtured_count < MAX_MRS)) || return 0
    if nurture_mr $project_id $mr_id; then
    ((nurtured_count++))
    fi
    done
    }

    usage() {
    echo "Usage:"
    echo "$0 GITLAB_ACCESS_TOKEN AUTHOR_ID FILTER 'name1 name2 name3' <MAX_MRS>"
    echo
    echo "examples:"
    echo "$0 df12fs1sdd 12345 'groups/789/search?scope=projects&search=' 'a b c' 3"
    echo "$0 df12fs1sdd 12345 'users/123/projects?search=' 'a b c'"
    }

    [ $# -gt 3 ] || usage

    GITLAB_PAT=$1
    AUTHOR_ID=$2
    PROJECTS_FILTER=$3
    PROJECT_NAMES="$4"
    MAX_MRS=${5:-1}

    for project_name in $PROJECT_NAMES ; do
    nurture_project $project_name
    done