diff --git a/README.md b/README.md
index 68a06ab..0c1116e 100644
--- a/README.md
+++ b/README.md
@@ -196,7 +196,7 @@ let g:vim_project_local_config = {
| R | roll back the selected files |
| s | shelve: save the changes as a patch and revert them |
| S | unshelve a shelf or one of its files |
-| u / p / P | pull, push, pull then push, in the background |
+| u / p / P | pull (keeping local changes with `--autostash`), push, pull then push, in the background |
| A | abort the merge, rebase, cherry-pick or revert in progress |
Merge conflicts: a pull or branch merge that stops on conflicts opens the Unmerged changelist. Enter on a file opens a three-pane merge window, yours | result | theirs, with each conflict colored by state. Press < yours, > theirs, = both, e edit, u undo, ]c / [c move, q to accept (write and `git add`) once all are resolved, Q to discard. After the last file it offers to commit the merge or continue the rebase.
diff --git a/autoload/project/config.vim b/autoload/project/config.vim
index 6672a5b..143ba25 100644
--- a/autoload/project/config.vim
+++ b/autoload/project/config.vim
@@ -39,6 +39,7 @@ let s:default = {
\'new_tasks_post_cmd': '',
\'commit_message': '',
\'git_branch_fetch': 1,
+ \'git_pull_autostash': 1,
\'find_replace_separator': ' => ',
\'debug': 0,
\}
diff --git a/autoload/project/git.vim b/autoload/project/git.vim
index 4a38464..0cd648f 100644
--- a/autoload/project/git.vim
+++ b/autoload/project/git.vim
@@ -2022,12 +2022,18 @@ endfunction
" a:1: called after a successful pull
function! s:TryPull(...)
let Then = get(a:000, 0, v:null)
- call s:RunGitInBackground('git pull', 'Updating...', 'Updated successfully',
+ " Like IntelliJ, local changes are put aside and back around the pull
+ let cmd = project#config#Get('git_pull_autostash') ? 'git pull --autostash' : 'git pull'
+ call s:RunGitInBackground(cmd, 'Updating...', 'Updated successfully',
\function('s:OnPulled', [Then]))
endfunction
function! s:OnPulled(Then)
call project#search_files#Reset()
+ " Putting the local changes back may conflict with what was pulled
+ if project#git_merge#ShowConflicts()
+ return
+ endif
if a:Then isnot v:null
call a:Then()
endif
diff --git a/autoload/project/git_merge.vim b/autoload/project/git_merge.vim
index 556e68b..f175737 100644
--- a/autoload/project/git_merge.vim
+++ b/autoload/project/git_merge.vim
@@ -34,11 +34,13 @@ function! project#git_merge#GetState()
return {}
endif
- if isdirectory(git_dir.'/rebase-merge') || isdirectory(git_dir.'/rebase-apply')
- return {'name': 'rebase', 'yours': 3, 'theirs': 2,
- \'continue': 'git -c core.editor=true rebase --continue',
- \'abort': 'git rebase --abort'}
- endif
+ for dir in ['rebase-merge', 'rebase-apply']
+ if isdirectory(git_dir.'/'.dir)
+ return {'name': 'rebase', 'yours': 3, 'theirs': 2, 'rebase_dir': git_dir.'/'.dir,
+ \'continue': 'git -c core.editor=true rebase --continue',
+ \'abort': 'git rebase --abort'}
+ endif
+ endfor
for [name, head] in [['merge', 'MERGE_HEAD'], ['cherry-pick', 'CHERRY_PICK_HEAD'], ['revert', 'REVERT_HEAD']]
if filereadable(git_dir.'/'.head)
let continue = name == 'merge'
@@ -48,6 +50,12 @@ function! project#git_merge#GetState()
\'continue': continue, 'abort': 'git '.name.' --abort'}
endif
endfor
+ " Nothing in progress, yet conflicts: applying a stash, like after pull
+ " --autostash. git's theirs is the stash, your local changes
+ if !empty(project#git_merge#GetUnmergedFiles())
+ \&& !empty(project#RunShellCmd('git stash list -n 1', 0))
+ return {'name': 'stash', 'yours': 3, 'theirs': 2}
+ endif
return {}
endfunction
@@ -59,19 +67,39 @@ endfunction
function! s:GetTitles(state)
let head = s:Describe('HEAD')
if get(a:state, 'name', '') == 'rebase'
- let rebase_head = s:Describe('REBASE_HEAD')
- return ['Yours (being rebased) '.rebase_head, 'Upstream (rebasing onto) '.head]
+ " HEAD is detached: name the branches from what git saved for the rebase
+ let branch = s:ReadRebaseFile(a:state, 'head-name')
+ let branch = substitute(branch, '^refs/heads/', '', '')
+ let rebase_head = s:ShortHash('REBASE_HEAD')
+ let yours = empty(branch) ? rebase_head : branch.' '.rebase_head
+ let onto = s:Describe(s:ReadRebaseFile(a:state, 'onto'))
+ return ['Yours (being rebased) '.yours, 'Upstream (rebasing onto) '.onto]
endif
if has_key(a:state, 'head')
return ['Yours '.head, 'Theirs '.s:Describe(a:state.head)]
endif
+ if get(a:state, 'name', '') == 'stash'
+ return ['Yours (local changes, stashed)', 'Upstream '.head]
+ endif
return ['Yours '.head, 'Theirs']
endfunction
" Branch name or short hash of a revision, like 'main a1b2c3d'
+function! s:ReadRebaseFile(state, name)
+ let path = get(a:state, 'rebase_dir', '').'/'.a:name
+ return filereadable(path) ? get(readfile(path), 0, '') : ''
+endfunction
+
+function! s:ShortHash(rev)
+ return get(project#RunShellCmd('git rev-parse --short '.shellescape(a:rev), 0), 0, '')
+endfunction
+
function! s:Describe(rev)
+ if empty(a:rev)
+ return ''
+ endif
let rev = shellescape(a:rev)
- let hash = get(project#RunShellCmd('git rev-parse --short '.rev, 0), 0, '')
+ let hash = s:ShortHash(a:rev)
if empty(hash)
return ''
endif
@@ -140,7 +168,71 @@ function! s:BuildSegments(left, base, right)
" It exits with the number of conflicts
let output = systemlist(cmd)
call delete(dir, 'rf')
- return s:ParseMergeOutput(output)
+ let segments = s:ParseMergeOutput(output)
+ call s:MarkAutoMerged(segments, a:left, a:base)
+ return segments
+endfunction
+
+" Mark the lines git merged on its own, which changed on one side only:
+" segment.auto maps a line index of a text segment to 'left', 'right' or
+" 'both'. Diff the base with the merged text, the conflicts kept as base,
+" then tell by yours whose change each one is
+function! s:MarkAutoMerged(segments, left, base)
+ let merged = []
+ let starts = []
+ for segment in a:segments
+ call add(starts, len(merged) + 1)
+ call extend(merged, segment.type == 'text' ? segment.lines : segment.base)
+ endfor
+
+ let yours = {}
+ for hunk in s:DiffHunks(a:base, a:left)
+ let yours[hunk.old] = hunk.lines
+ endfor
+
+ for hunk in s:DiffHunks(a:base, merged)
+ if !has_key(yours, hunk.old)
+ let side = 'right'
+ else
+ let side = yours[hunk.old] == hunk.lines ? 'left' : 'both'
+ endif
+ for lnum in range(hunk.start, hunk.start + len(hunk.lines) - 1)
+ for idx in range(len(a:segments))
+ let segment = a:segments[idx]
+ let offset = lnum - starts[idx]
+ if segment.type == 'text' && offset >= 0 && offset < len(segment.lines)
+ if !has_key(segment, 'auto')
+ let segment.auto = {}
+ endif
+ let segment.auto[offset] = side
+ endif
+ endfor
+ endfor
+ endfor
+endfunction
+
+" Hunks of changed lines from a:old to a:new: {'old': 'start,count' in
+" a:old, 'start': first line in a:new, 'lines': the new lines}
+function! s:DiffHunks(old, new)
+ let dir = tempname()
+ call mkdir(dir, 'p')
+ call writefile(a:old, dir.'/old')
+ call writefile(a:new, dir.'/new')
+ let output = systemlist('git diff --no-index --no-color --no-ext-diff -U0 -- '
+ \.shellescape(dir.'/old').' '.shellescape(dir.'/new'))
+ call delete(dir, 'rf')
+
+ let hunks = []
+ for line in output
+ let m = matchlist(line, '^@@ -\(\d\+\)\%(,\(\d\+\)\)\? +\(\d\+\)')
+ if !empty(m)
+ let num = empty(m[2]) ? 1 : str2nr(m[2])
+ call add(hunks, {'old': m[1].','.num, 'start': str2nr(m[3]), 'lines': []})
+ elseif !empty(hunks) && line[0] ==# '+'
+ call add(hunks[-1].lines, line[1:])
+ endif
+ endfor
+ return hunks
endfunction
function! s:Conflicts()
@@ -192,6 +284,7 @@ function! project#git_merge#Open(file)
\'segments': s:BuildSegments(yours, base is v:null ? [] : base, theirs),
\'left_title': left_title,
\'right_title': right_title,
+ \'state': state,
\}
call s:OpenWindows()
call s:Render()
@@ -311,6 +404,18 @@ function! s:IsMapped(key)
return !empty(a:key) && a:key !=? ''
endfunction
+" Like 'q to accept, Q to discard', with the configured keys
+function! s:AcceptDiscardHint()
+ let hints = []
+ for [name, label] in [['finish', 'accept'], ['quit', 'discard']]
+ let key = s:GetMergeKey(name)
+ if !empty(key)
+ call add(hints, key.' to '.label)
+ endif
+ endfor
+ return join(hints, ', ')
+endfunction
+
" Key of a:name in the merge window, '' when it has none
function! s:GetMergeKey(name)
return s:GetHelpKey(project#config#Get('git_merge_mappings'), a:name)
@@ -338,6 +443,7 @@ function! s:DefineHighlights()
hi default ProjectMergePending ctermbg=58 guibg=#4f4000
hi default link ProjectMergeUnused Comment
hi default link ProjectMergeFiller NonText
+ hi default link ProjectMergeAuto Comment
let utf8 = &encoding ==# 'utf-8'
let signs = {
@@ -349,6 +455,9 @@ function! s:DefineHighlights()
\'used': [utf8 ? '✓' : 'ok', 'ProjectMergeResolved'],
\'unused': ['--', 'ProjectMergeUnused'],
\'filler': [' ', 'ProjectMergeFiller'],
+ \'auto_left': [utf8 ? '◂' : '<~', 'ProjectMergeAuto'],
+ \'auto_right': [utf8 ? '▸' : '~>', 'ProjectMergeAuto'],
+ \'auto_both': [utf8 ? '◆' : '<>', 'ProjectMergeAuto'],
\}
for [name, value] in items(signs)
call sign_define('ProjectMerge_'.name, {'text': value[0], 'linehl': value[1], 'texthl': value[1]})
@@ -380,6 +489,17 @@ function! s:Render()
for pane in panes
call extend(pane, segment.lines)
endfor
+ " Merged by git: grey in the result and on the side it came from
+ for [offset, side] in items(get(segment, 'auto', {}))
+ let lnum = start + offset
+ call add(signs[1], [lnum, 'auto_'.side])
+ if side != 'right'
+ call add(signs[0], [lnum, 'auto_'.side])
+ endif
+ if side != 'left'
+ call add(signs[2], [lnum, 'auto_'.side])
+ endif
+ endfor
call add(s:layout, {'start': start, 'height': len(segment.lines)})
continue
endif
@@ -422,9 +542,13 @@ function! s:UpdateStatusLines()
let current = s:GetConflictNumber()
let unresolved = s:CountUnresolved()
" Shorten the file name first when the pane is narrow
- let status = printf(' %s/%d %s %%<%s', current ? current : '-', total,
- \unresolved ? unresolved.' unresolved' : 'all resolved, '.s:GetMergeKey('finish').' to accept',
- \s:EscapeStatus(s:merge.file))
+ if unresolved
+ let status = printf(' %s/%d %d unresolved %%<%s', current ? current : '-', total,
+ \unresolved, s:EscapeStatus(s:merge.file))
+ else
+ " Short enough for a narrow pane: what to press matters more here
+ let status = ' done: '.s:AcceptDiscardHint().' %<'.s:EscapeStatus(s:merge.file)
+ endif
call s:SetStatusLine(s:left_buffer, ' '.s:EscapeStatus(s:merge.left_title))
call s:SetStatusLine(s:result_buffer, status)
call s:SetStatusLine(s:right_buffer, ' '.s:EscapeStatus(s:merge.right_title))
@@ -515,7 +639,7 @@ function! s:Take(state)
call s:Render()
call s:CursorTo(idx)
if !empty(a:state) && !s:JumpToConflict(1, 1)
- call project#Info('All conflicts resolved, press '.s:GetMergeKey('finish').' to accept')
+ call project#Info('All conflicts resolved, '.s:AcceptDiscardHint())
endif
endfunction
@@ -553,7 +677,20 @@ function! s:EditConflict()
call setline(1, lines)
setlocal nomodified
let b:vim_project_merge_segment = idx
- setlocal statusline=\ Edit\ the\ conflict,\ :w\ to\ use\ it,\ :q!\ to\ cancel
+ " The same keys as the merge window: accept or discard, :w and :q! work too
+ let accept = s:GetMergeKey('finish')
+ let discard = s:GetMergeKey('quit')
+ let hints = []
+ if !empty(accept)
+ execute 'nnoremap '.accept.' :silent write'
+ call add(hints, accept.' to accept')
+ endif
+ if !empty(discard)
+ execute 'nnoremap '.discard.' :bwipeout!'
+ call add(hints, discard.' to discard')
+ endif
+ let &l:statusline = ' Edit the conflict'
+ \.(empty(hints) ? ', :w to use it, :q! to cancel' : ', '.join(hints, ', '))
autocmd BufWriteCmd call s:SaveEditedConflict()
" Back to the result however it is closed: :w, :wq, :x or :q!
autocmd BufWipeout call timer_start(0, {-> s:BackToResult()})
@@ -607,17 +744,23 @@ function! s:Finish()
let path = $vim_project.'/'.s:merge.file
call writefile(s:GetResultLines(), path)
- call project#RunShellCmd('git add -- '.shellescape(s:merge.file))
+ let file = shellescape(s:merge.file)
+ call project#RunShellCmd('git add -- '.file)
if v:shell_error
return
endif
+ let state = s:merge.state
+ if get(state, 'name', '') == 'stash'
+ " Back to a local change, as applying the stash without conflicts would
+ call project#RunShellCmd('git reset -q -- '.file)
+ endif
let bufnr = bufnr(path)
if bufnr != -1
silent! execute 'checktime' bufnr
endif
call s:CloseWindows()
- call s:OnFileResolved()
+ call s:OnFileResolved(state)
endfunction
function! s:Quit()
@@ -646,12 +789,18 @@ endfunction
" Back to the local changes. Once no file is left, offer to finish what git
" is in the middle of
-function! s:OnFileResolved()
+" a:1: the state the file was resolved in
+function! s:OnFileResolved(...)
call project#git#Status()
if !empty(project#git_merge#GetUnmergedFiles())
call project#Info('Resolved. Press '.s:GetKey('open_changelist_or_file').' on the next conflicting file')
return
endif
+ if get(get(a:000, 0, {}), 'name', '') == 'stash'
+ call project#Info('All conflicts resolved. Your changes are still in the stash too: '
+ \.'git stash drop once you are sure')
+ return
+ endif
let state = project#git_merge#GetState()
if empty(state)
@@ -687,6 +836,11 @@ function! project#git_merge#Abort()
call project#Info('No merge, rebase, cherry-pick or revert in progress')
return
endif
+ if !has_key(state, 'abort')
+ call project#Info('Applying the stash has conflicts. Your changes are safe in it: '
+ \.'resolve them, or git checkout the files and git stash pop again later')
+ return
+ endif
redraw
echo 'Abort the '.state.name.' and go back to before it? (y/n) '
let answer = nr2char(getchar())
diff --git a/doc/vim-project.txt b/doc/vim-project.txt
index dd2e5bd..d65ae90 100644
--- a/doc/vim-project.txt
+++ b/doc/vim-project.txt
@@ -269,12 +269,16 @@ in a new tab, like the Merge dialog of IntelliJ IDEA:
Panes are titled with the branch and commit they come from; in a rebase
yours is the commit being rebased. Conflicts line up across the panes and
are colored by state: unresolved, taken from a side, or edited by hand.
+Lines git merged on its own, changed on one side only, are grey
+("ProjectMergeAuto") and marked with the side they came from: ◂ yours,
+▸ theirs.
All panes are read-only, so a choice can always be changed. Keys work on
the conflict under the cursor, and are listed at the bottom of the window
("git_merge_mappings"):
< take yours > take theirs
- = take both e edit it by hand, :w to use it
+ = take both e edit it by hand, then q to
+ accept or Q to discard
u back to unresolved ]c [c next or previous conflict
L R take yours or theirs for all conflicts
q accept: write the file, git add it and close. Refused
@@ -288,6 +292,10 @@ rebase, cherry-pick or revert.
Pull and push run in the background; the result is shown when done. git
cannot ask for credentials there, so use a credential helper or ssh-agent.
+Pull runs with --autostash: local changes are put aside and back around it.
+When putting them back conflicts with what was pulled, they stay safe in
+the stash and the merge window opens, with your local changes as yours.
+Turn it off with "git_pull_autostash".
In the diff buffer, jumps to the source line ("git_diff_mappings").
In the changes buffer, opens the file ("git_changes_mappings").
@@ -322,6 +330,9 @@ check_branch_when_use_session 0
git_branch_fetch 1
Fetch all remotes in the background when |:ProjectGitBranch| opens.
+git_pull_autostash 1
+ Pull with --autostash, keeping local changes across it. Needs git 2.27.
+
open_root_when_use_session 0
Open the project root even when a session is loaded.
diff --git a/test/cases/git_merge_test.vim b/test/cases/git_merge_test.vim
index 41fb4c5..0fa6c92 100644
--- a/test/cases/git_merge_test.vim
+++ b/test/cases/git_merge_test.vim
@@ -11,7 +11,9 @@ endfunction
" A project in the middle of merging 'feature', which conflicts with the
" current branch on lines 2 and 4 of file.txt
-function! s:OpenConflict(name)
+function! s:OpenConflict(name, ...)
+ " 'merge', 'rebase' or 'cherry-pick' feature
+ let operation = get(a:000, 0, 'merge')
let base = Helper_TmpDir(a:name)
let repo = base.'/proj'
call mkdir(repo, 'p')
@@ -27,7 +29,7 @@ function! s:OpenConflict(name)
call s:Git(repo, 'checkout -q -')
call writefile(['a', 'B yours', 'c', 'D yours', 'e'], repo.'/file.txt')
call s:Git(repo, 'commit -q -am mine')
- call s:Git(repo, 'merge feature')
+ call s:Git(repo, operation.' feature')
let project = {'name': 'proj', 'path': base, 'fullpath': repo, 'note': '', 'option': {}}
call Helper_Call('autoload/project.vim', 'InitProjectConfig', [project])
@@ -36,7 +38,9 @@ function! s:OpenConflict(name)
endfunction
function! s:Close(repo)
- call s:Git(a:repo, 'merge --abort')
+ for operation in ['merge', 'rebase', 'cherry-pick']
+ call s:Git(a:repo, operation.' --abort')
+ endfor
silent! call s:Call('CloseWindows')
silent! execute 'bwipeout!' bufnr('local_changes')
call project#QuitProject()
@@ -167,6 +171,23 @@ function! Test_edit_conflict_by_hand()
call assert_notequal(-1, bufwinnr('vim_project_merge_yours'), cmd)
call assert_notequal(-1, bufwinnr('vim_project_merge_theirs'), cmd)
endfor
+
+ " q and Q accept and discard, like in the merge window
+ call cursor(5, 1)
+ normal e
+ call assert_match('q to accept, Q to discard', &l:statusline)
+ call setline(1, ['D by q'])
+ normal q
+ sleep 50m
+ call assert_equal('vim_project_merge_result', bufname('%'))
+ call assert_equal('D by q', getline(5))
+ call cursor(5, 1)
+ normal e
+ call setline(1, ['D dropped'])
+ normal Q
+ sleep 50m
+ call assert_equal('vim_project_merge_result', bufname('%'))
+ call assert_equal('D by q', getline(5))
finally
call s:Close(repo)
endtry
@@ -230,3 +251,53 @@ function! Test_abort_merge()
call s:Close(repo)
endtry
endfunction
+
+function! Test_rebase_shows_the_rebased_commit_as_yours()
+ let repo = s:OpenConflict('merge rebase', 'rebase')
+ try
+ call assert_equal('rebase', project#git_merge#GetState().name)
+ call project#git_merge#Open('file.txt')
+ " git's ours is feature, being rebased onto: yours is still on the left
+ call assert_equal(['a', 'B yours', 'c', 'D yours', 'e'], s:Lines('vim_project_merge_yours'))
+ call assert_equal(['a', 'B theirs', 'c', 'D theirs', 'e'], s:Lines('vim_project_merge_theirs'))
+ let titles = s:Call('GetTitles', project#git_merge#GetState())
+ call assert_match('^Yours (being rebased) .* ', titles[0])
+ call assert_match('^Upstream (rebasing onto) feature ', titles[1])
+
+ " Continue finishes the rebase
+ call feedkeys('Lyqy', 'xt')
+ call assert_equal({}, project#git_merge#GetState())
+ call assert_equal(['a', 'B yours', 'c', 'D yours', 'e'], readfile(repo.'/file.txt'))
+ call assert_equal(['mine', 'feature'], split(s:Git(repo, 'log --format=%s -2'), "\n"))
+ finally
+ call s:Close(repo)
+ endtry
+endfunction
+
+function! Test_cherry_pick_conflict_and_continue()
+ let repo = s:OpenConflict('merge cherry-pick', 'cherry-pick')
+ try
+ call assert_equal('cherry-pick', project#git_merge#GetState().name)
+ call project#git_merge#Open('file.txt')
+ call assert_equal(['a', 'B yours', 'c', 'D yours', 'e'], s:Lines('vim_project_merge_yours'))
+ call assert_match('^Theirs ', s:Call('GetTitles', project#git_merge#GetState())[1])
+
+ call feedkeys('Ryqy', 'xt')
+ call assert_equal({}, project#git_merge#GetState())
+ call assert_equal(['a', 'B theirs', 'c', 'D theirs', 'e'], readfile(repo.'/file.txt'))
+ call assert_equal('feature', trim(s:Git(repo, 'log --format=%s -1')))
+ finally
+ call s:Close(repo)
+ endtry
+endfunction
+
+function! Test_MarkAutoMerged_tells_whose_change_git_merged()
+ let base = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+ " Yours changes a and d, theirs changes g and d: d conflicts
+ let yours = ['A yours', 'b', 'c', 'D yours', 'e', 'f', 'g']
+ let theirs = ['a', 'b', 'c', 'D theirs', 'e', 'f', 'G theirs']
+ let segments = s:Call('BuildSegments', yours, base, theirs)
+ call assert_equal(['text', 'conflict', 'text'], map(copy(segments), 'v:val.type'))
+ call assert_equal({'0': 'left'}, segments[0].auto)
+ call assert_equal({'2': 'right'}, segments[2].auto)
+endfunction
diff --git a/test/cases/git_test.vim b/test/cases/git_test.vim
index 6218eb0..f54ef00 100644
--- a/test/cases/git_test.vim
+++ b/test/cases/git_test.vim
@@ -494,3 +494,55 @@ function! Test_GenerateBrief_keeps_the_message_whole_and_the_splitter_on_one_row
call assert_equal(revision.message, brief[1])
call assert_true(strdisplaywidth(brief[0]) <= s:Call('GetChangesTextWidth'))
endfunction
+
+" Pull puts local changes aside and back, like IntelliJ
+function! Test_pull_keeps_local_changes_with_autostash()
+ let base = s:Remote('pull-autostash')
+ call s:WithProject(base.'/mine', function('s:CheckPullAutostash', [base]))
+endfunction
+
+function! s:CheckPullAutostash(base)
+ let mine = a:base.'/mine'
+ let theirs = a:base.'/theirs'
+ let mine_git = 'git -C '.shellescape(mine).' '
+
+ " A change in a file the pull also changes, without conflicting lines
+ call writefile(['a', 'b', 'c', 'd', 'e'], theirs.'/shared.txt')
+ call system('git -C '.shellescape(theirs).' add . && git -C '.shellescape(theirs).' commit -q -m shared && git -C '.shellescape(theirs).' push -q')
+ call system(mine_git.'pull -q')
+ call writefile(['a', 'b theirs', 'c', 'd', 'e'], theirs.'/shared.txt')
+ call system('git -C '.shellescape(theirs).' commit -q -am theirs && git -C '.shellescape(theirs).' push -q')
+ call writefile(['a', 'b', 'c', 'd', 'e mine'], mine.'/shared.txt')
+
+ call s:Call('TryPull')
+ call s:WaitGitJob()
+ call assert_equal(s:RemoteHead(a:base), s:Head(mine))
+ call assert_equal(['a', 'b theirs', 'c', 'd', 'e mine'], readfile(mine.'/shared.txt'))
+ call assert_equal('', system(mine_git.'stash list'))
+
+ " A conflicting change: kept in the stash, resolved in the merge window
+ call writefile(['a', 'b theirs 2', 'c', 'd', 'e'], theirs.'/shared.txt')
+ call system('git -C '.shellescape(theirs).' commit -q -am theirs2 && git -C '.shellescape(theirs).' push -q')
+ call writefile(['a', 'b mine', 'c', 'd', 'e mine'], mine.'/shared.txt')
+ call s:Call('TryPull')
+ call s:WaitGitJob()
+ call assert_equal(s:RemoteHead(a:base), s:Head(mine))
+ call assert_equal(['shared.txt'], project#git_merge#GetUnmergedFiles())
+ call assert_equal('stash', project#git_merge#GetState().name)
+
+ try
+ call project#git_merge#Open('shared.txt')
+ " Yours are the local changes, not what was pulled
+ call assert_equal(['a', 'b mine', 'c', 'd', 'e mine'], getbufline(bufnr('vim_project_merge_yours'), 1, '$'))
+ call feedkeys(' f.txt
+printf 'a\nb\nc\nd\ne\nf\ng\nh\n' > f.txt
git add f.txt
git commit -q -m init
@@ -40,13 +41,13 @@ if [ "$mode" = delete ]; then
git rm -q f.txt
git commit -q -m 'feature: delete f.txt'
else
- printf 'a\nB theirs\nc\nD theirs\ne\n' > f.txt
- git commit -q -am 'feature: change b and d'
+ printf 'a\nB theirs\nc\nD theirs\ne\nF theirs\ng\nh\n' > f.txt
+ git commit -q -am 'feature: change b, d and f'
fi
git checkout -q main
-printf 'a\nB yours\nc\nD yours\ne\n' > f.txt
-git commit -q -am 'main: change b and d'
+printf 'a\nB yours\nc\nD yours\ne\nf\ng\nH yours\n' > f.txt
+git commit -q -am 'main: change b, d and h'
cmd=merge
[ "$mode" = rebase ] && cmd=rebase