Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2893167
ArcanistGitLandEngine.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
38 KB
Referenced Files
None
Subscribers
None
ArcanistGitLandEngine.php
View Options
<?php
final
class
ArcanistGitLandEngine
extends
ArcanistLandEngine
{
private
$isGitPerforce
;
private
$landTargetCommitMap
=
array
(
)
;
private
$deletedBranches
=
array
(
)
;
private
function
setIsGitPerforce
(
$is_git_perforce
)
{
$this
->
isGitPerforce
=
$is_git_perforce
;
return
$this
;
}
private
function
getIsGitPerforce
(
)
{
return
$this
->
isGitPerforce
;
}
protected
function
pruneBranches
(
array
$sets
)
{
$old_commits
=
array
(
)
;
foreach
(
$sets
as
$set
)
{
$hash
=
last
(
$set
->
getCommits
(
)
)
->
getHash
(
)
;
$old_commits
[
]
=
$hash
;
}
$branch_map
=
$this
->
getBranchesForCommits
(
$old_commits
,
$is_contains
=
false
)
;
$api
=
$this
->
getRepositoryAPI
(
)
;
foreach
(
$branch_map
as
$branch_name
=>
$branch_hash
)
{
$recovery_command
=
csprintf
(
'git checkout -b %s %s'
,
$branch_name
,
$this
->
getDisplayHash
(
$branch_hash
)
)
;
echo
tsprintf
(
"%s\n"
,
pht
(
'Cleaning up branch "%s"...'
,
$branch_name
)
)
;
echo
tsprintf
(
"%s\n"
,
pht
(
'(Use `%s` if you want it back.)'
,
$recovery_command
)
)
;
$api
->
execxLocal
(
'branch -D -- %s'
,
$branch_name
)
;
$this
->
deletedBranches
[
$branch_name
]
=
true
;
}
}
private
function
getBranchesForCommits
(
array
$hashes
,
$is_contains
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$format
=
'%(refname) %(objectname)'
;
$result
=
array
(
)
;
foreach
(
$hashes
as
$hash
)
{
if
(
$is_contains
)
{
$command
=
csprintf
(
'for-each-ref --contains %s --format %s --'
,
$hash
,
$format
)
;
}
else
{
$command
=
csprintf
(
'for-each-ref --points-at %s --format %s --'
,
$hash
,
$format
)
;
}
list
(
$foreach_lines
)
=
$api
->
execxLocal
(
'%C'
,
$command
)
;
$foreach_lines
=
phutil_split_lines
(
$foreach_lines
,
false
)
;
foreach
(
$foreach_lines
as
$line
)
{
if
(
!
strlen
(
$line
)
)
{
continue
;
}
$expect_parts
=
2
;
$parts
=
explode
(
' '
,
$line
,
$expect_parts
)
;
if
(
count
(
$parts
)
!==
$expect_parts
)
{
throw
new
Exception
(
pht
(
'Failed to explode line "%s".'
,
$line
)
)
;
}
$ref_name
=
$parts
[
0
]
;
$ref_hash
=
$parts
[
1
]
;
$matches
=
null
;
$ok
=
preg_match
(
'(^refs/heads/(.*)\z)'
,
$ref_name
,
$matches
)
;
if
(
$ok
===
false
)
{
throw
new
Exception
(
pht
(
'Failed to match against branch pattern "%s".'
,
$line
)
)
;
}
if
(
!
$ok
)
{
continue
;
}
$result
[
$matches
[
1
]
]
=
$ref_hash
;
}
}
// Sort the result so that branches are processed in natural order.
$names
=
array_keys
(
$result
)
;
natcasesort
(
$names
)
;
$result
=
array_select_keys
(
$result
,
$names
)
;
return
$result
;
}
protected
function
cascadeState
(
ArcanistLandCommitSet
$set
,
$into_commit
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
// This has no effect when we're executing a merge strategy.
if
(
!
$this
->
isSquashStrategy
(
)
)
{
return
;
}
$old_commit
=
last
(
$set
->
getCommits
(
)
)
->
getHash
(
)
;
$new_commit
=
$into_commit
;
$branch_map
=
$this
->
getBranchesForCommits
(
array
(
$old_commit
)
,
$is_contains
=
true
)
;
$log
=
$this
->
getLogEngine
(
)
;
foreach
(
$branch_map
as
$branch_name
=>
$branch_head
)
{
// If this branch just points at the old state, don't bother rebasing
// it. We'll update or delete it later.
if
(
$branch_head
===
$old_commit
)
{
continue
;
}
$log
->
writeStatus
(
pht
(
'CASCADE'
)
,
pht
(
'Rebasing "%s" onto landed state...'
,
$branch_name
)
)
;
try
{
$api
->
execxLocal
(
'rebase --onto %s -- %s %s'
,
$new_commit
,
$old_commit
,
$branch_name
)
;
}
catch
(
CommandException
$ex
)
{
// TODO: If we have a stashed state or are not running in incremental
// mode: abort the rebase, restore the local state, and pop the stash.
// Otherwise, drop the user out here.
throw
$ex
;
}
}
}
private
function
fetchTarget
(
ArcanistLandTarget
$target
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
// NOTE: Although this output isn't hugely useful, we need to passthru
// instead of using a subprocess here because `git fetch` may prompt the
// user to enter a password if they're fetching over HTTP with basic
// authentication. See T10314.
if
(
$this
->
getIsGitPerforce
(
)
)
{
$log
->
writeStatus
(
pht
(
'P4 SYNC'
)
,
pht
(
'Synchronizing "%s" from Perforce...'
,
$target
->
getRef
(
)
)
)
;
$err
=
$api
->
execPassthru
(
'p4 sync --silent --branch %s --'
,
$target
->
getRemote
(
)
.
'/'
.
$target
->
getRef
(
)
)
;
if
(
$err
)
{
throw
new
ArcanistUsageException
(
pht
(
'Perforce sync failed! Fix the error and run "arc land" again.'
)
)
;
}
return
$this
->
getLandTargetLocalCommit
(
$target
)
;
}
$exists
=
$this
->
getLandTargetLocalExists
(
$target
)
;
if
(
!
$exists
)
{
$log
->
writeWarning
(
pht
(
'TARGET'
)
,
pht
(
'No local copy of ref "%s" in remote "%s" exists, attempting '
.
'fetch...'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
$this
->
fetchLandTarget
(
$target
,
$ignore_failure
=
true
)
;
$exists
=
$this
->
getLandTargetLocalExists
(
$target
)
;
if
(
!
$exists
)
{
return
null
;
}
$log
->
writeStatus
(
pht
(
'FETCHED'
)
,
pht
(
'Fetched ref "%s" from remote "%s".'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
return
$this
->
getLandTargetLocalCommit
(
$target
)
;
}
$log
->
writeStatus
(
pht
(
'FETCH'
)
,
pht
(
'Fetching "%s" from remote "%s"...'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
$this
->
fetchLandTarget
(
$target
,
$ignore_failure
=
false
)
;
return
$this
->
getLandTargetLocalCommit
(
$target
)
;
}
private
function
updateWorkingCopy
(
$into_commit
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
if
(
$into_commit
===
null
)
{
throw
new
Exception
(
'TODO: Author a new empty state.'
)
;
}
else
{
$api
->
execxLocal
(
'checkout %s --'
,
$into_commit
)
;
}
}
protected
function
executeMerge
(
ArcanistLandCommitSet
$set
,
$into_commit
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
$this
->
updateWorkingCopy
(
$into_commit
)
;
$commits
=
$set
->
getCommits
(
)
;
$max_commit
=
last
(
$commits
)
;
$source_commit
=
$max_commit
->
getHash
(
)
;
// NOTE: See T11435 for some history. See PHI1727 for a case where a user
// modified their working copy while running "arc land". This attempts to
// resist incorrectly detecting simultaneous working copy modifications
// as changes.
list
(
$changes
)
=
$api
->
execxLocal
(
'diff --no-ext-diff HEAD..%s --'
,
$source_commit
)
;
$changes
=
trim
(
$changes
)
;
if
(
!
strlen
(
$changes
)
)
{
// TODO: We could make a more significant effort to identify the
// human-readable symbol which led us to try to land this ref.
throw
new
PhutilArgumentUsageException
(
pht
(
'Merging local "%s" into "%s" produces an empty diff. '
.
'This usually means these changes have already landed.'
,
$this
->
getDisplayHash
(
$source_commit
)
,
$this
->
getDisplayHash
(
$into_commit
)
)
)
;
}
$log
->
writeStatus
(
pht
(
'MERGING'
)
,
pht
(
'%s %s'
,
$this
->
getDisplayHash
(
$source_commit
)
,
$max_commit
->
getDisplaySummary
(
)
)
)
;
try
{
if
(
$this
->
isSquashStrategy
(
)
)
{
// NOTE: We're explicitly specifying "--ff" to override the presence
// of "merge.ff" options in user configuration.
$api
->
execxLocal
(
'merge --no-stat --no-commit --ff --squash -- %s'
,
$source_commit
)
;
}
else
{
$api
->
execxLocal
(
'merge --no-stat --no-commit --no-ff -- %s'
,
$source_commit
)
;
}
}
catch
(
CommandException
$ex
)
{
// TODO: If we previously succeeded with at least one merge, we could
// provide a hint that "--incremental" can do some of the work.
$api
->
execManualLocal
(
'merge --abort'
)
;
$api
->
execManualLocal
(
'reset --hard HEAD --'
)
;
$direct_symbols
=
$max_commit
->
getDirectSymbols
(
)
;
$indirect_symbols
=
$max_commit
->
getIndirectSymbols
(
)
;
if
(
$direct_symbols
)
{
$message
=
pht
(
'Local commit "%s" (%s) does not merge cleanly into "%s". '
.
'Merge or rebase local changes so they can merge cleanly.'
,
$this
->
getDisplayHash
(
$source_commit
)
,
$this
->
getDisplaySymbols
(
$direct_symbols
)
,
$this
->
getDisplayHash
(
$into_commit
)
)
;
}
else
if
(
$indirect_symbols
)
{
$message
=
pht
(
'Local commit "%s" (reachable from: %s) does not merge cleanly '
.
'into "%s". Merge or rebase local changes so they can merge '
.
'cleanly.'
,
$this
->
getDisplayHash
(
$source_commit
)
,
$this
->
getDisplaySymbols
(
$indirect_symbols
)
,
$this
->
getDisplayHash
(
$into_commit
)
)
;
}
else
{
$message
=
pht
(
'Local commit "%s" does not merge cleanly into "%s". Merge or '
.
'rebase local changes so they can merge cleanly.'
,
$this
->
getDisplayHash
(
$source_commit
)
,
$this
->
getDisplayHash
(
$into_commit
)
)
;
}
throw
new
PhutilArgumentUsageException
(
$message
)
;
}
list
(
$original_author
,
$original_date
)
=
$this
->
getAuthorAndDate
(
$source_commit
)
;
$revision_ref
=
$set
->
getRevisionRef
(
)
;
$commit_message
=
$revision_ref
->
getCommitMessage
(
)
;
$future
=
$api
->
execFutureLocal
(
'commit --author %s --date %s -F - --'
,
$original_author
,
$original_date
)
;
$future
->
write
(
$commit_message
)
;
$future
->
resolvex
(
)
;
list
(
$stdout
)
=
$api
->
execxLocal
(
'rev-parse --verify %s'
,
'HEAD'
)
;
$new_cursor
=
trim
(
$stdout
)
;
if
(
$into_commit
===
null
)
{
if
(
$this
->
isSquashStrategy
(
)
)
{
throw
new
Exception
(
pht
(
'TODO: Rewrite HEAD to have no parents.'
)
)
;
}
else
{
throw
new
Exception
(
pht
(
'TODO: Rewrite HEAD to have only source as a parent.'
)
)
;
}
}
return
$new_cursor
;
}
protected
function
pushChange
(
$into_commit
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
if
(
$this
->
getIsGitPerforce
(
)
)
{
// TODO: Specifying "--onto" more than once is almost certainly an error
// in Perforce.
$log
->
writeStatus
(
pht
(
'SUBMITTING'
)
,
pht
(
'Submitting changes to "%s".'
,
$this
->
getOntoRemote
(
)
)
)
;
$config_argv
=
array
(
)
;
// Skip the "git p4 submit" interactive editor workflow. We expect
// the commit message that "arc land" has built to be satisfactory.
$config_argv
[
]
=
'-c'
;
$config_argv
[
]
=
'git-p4.skipSubmitEdit=true'
;
// Skip the "git p4 submit" confirmation prompt if the user does not edit
// the submit message.
$config_argv
[
]
=
'-c'
;
$config_argv
[
]
=
'git-p4.skipSubmitEditCheck=true'
;
$flags_argv
=
array
(
)
;
// Disable implicit "git p4 rebase" as part of submit. We're allowing
// the implicit "git p4 sync" to go through since this puts us in a
// state which is generally similar to the state after "git push", with
// updated remotes.
// We could do a manual "git p4 sync" with a more narrow "--branch"
// instead, but it's not clear that this is beneficial.
$flags_argv
[
]
=
'--disable-rebase'
;
// Detect moves and submit them to Perforce as move operations.
$flags_argv
[
]
=
'-M'
;
// If we run into a conflict, abort the operation. We expect users to
// fix conflicts and run "arc land" again.
$flags_argv
[
]
=
'--conflict=quit'
;
$err
=
$api
->
execPassthru
(
'%LR p4 submit %LR --commit %R --'
,
$config_argv
,
$flags_argv
,
$into_commit
)
;
if
(
$err
)
{
throw
new
ArcanistUsageException
(
pht
(
'Submit failed! Fix the error and run "arc land" again.'
)
)
;
}
return
;
}
$log
->
writeStatus
(
pht
(
'PUSHING'
)
,
pht
(
'Pushing changes to "%s".'
,
$this
->
getOntoRemote
(
)
)
)
;
$refspecs
=
array
(
)
;
foreach
(
$this
->
getOntoRefs
(
)
as
$onto_ref
)
{
$refspecs
[
]
=
sprintf
(
'%s:%s'
,
$into_commit
,
$onto_ref
)
;
}
$err
=
$api
->
execPassthru
(
'push -- %s %Ls'
,
$this
->
getOntoRemote
(
)
,
$refspecs
)
;
if
(
$err
)
{
throw
new
ArcanistUsageException
(
pht
(
'Push failed! Fix the error and run "arc land" again.'
)
)
;
}
// TODO
// if ($this->isGitSvn) {
// $err = phutil_passthru('git svn dcommit');
// $cmd = 'git svn dcommit';
}
protected
function
reconcileLocalState
(
$into_commit
,
ArcanistRepositoryLocalState
$state
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getWorkflow
(
)
->
getLogEngine
(
)
;
// Try to put the user into the best final state we can. This is very
// complicated because users are incredibly creative and their local
// branches may, for example, have the same names as branches in the
// remote but no relationship to them.
// First, we're going to try to update these local branches:
//
// - the branch we started on originally; and
// - the local upstreams of the branch we started on originally; and
// - the local branch with the same name as the "into" ref; and
// - the local branch with the same name as the "onto" ref.
//
// These branches may not all exist and may not all be unique.
//
// To be updated, these branches must:
//
// - exist;
// - have not been deleted; and
// - be connected to the remote we pushed into.
$update_branches
=
array
(
)
;
$local_ref
=
$state
->
getLocalRef
(
)
;
if
(
$local_ref
!==
null
)
{
$update_branches
[
]
=
$local_ref
;
}
$local_path
=
$state
->
getLocalPath
(
)
;
if
(
$local_path
)
{
foreach
(
$local_path
->
getLocalBranches
(
)
as
$local_branch
)
{
$update_branches
[
]
=
$local_branch
;
}
}
if
(
!
$this
->
getIntoEmpty
(
)
&&
!
$this
->
getIntoLocal
(
)
)
{
$update_branches
[
]
=
$this
->
getIntoRef
(
)
;
}
foreach
(
$this
->
getOntoRefs
(
)
as
$onto_ref
)
{
$update_branches
[
]
=
$onto_ref
;
}
$update_branches
=
array_fuse
(
$update_branches
)
;
// Remove any branches we know we deleted.
foreach
(
$update_branches
as
$key
=>
$update_branch
)
{
if
(
isset
(
$this
->
deletedBranches
[
$update_branch
]
)
)
{
unset
(
$update_branches
[
$key
]
)
;
}
}
// Now, remove any branches which don't actually exist.
foreach
(
$update_branches
as
$key
=>
$update_branch
)
{
list
(
$err
)
=
$api
->
execManualLocal
(
'rev-parse --verify %s'
,
$update_branch
)
;
if
(
$err
)
{
unset
(
$update_branches
[
$key
]
)
;
}
}
$is_perforce
=
$this
->
getIsGitPerforce
(
)
;
if
(
$is_perforce
)
{
// If we're in Perforce mode, we don't expect to have a meaningful
// path to the remote: the "p4" remote is not a real remote, and
// "git p4" commands do not configure branch upstreams to provide
// a path.
// Additionally, we've already set the remote to the right state with an
// implicit "git p4 sync" during "git p4 submit", and "git pull" isn't a
// meaningful operation.
// We're going to skip everything here and just switch to the most
// desirable branch (if we can find one), then reset the state (if that
// operation is safe).
if
(
!
$update_branches
)
{
$log
->
writeStatus
(
pht
(
'DETACHED HEAD'
)
,
pht
(
'Unable to find any local branches to update, staying on '
.
'detached head.'
)
)
;
$state
->
discardLocalState
(
)
;
return
;
}
$dst_branch
=
head
(
$update_branches
)
;
if
(
!
$this
->
isAncestorOf
(
$dst_branch
,
$into_commit
)
)
{
$log
->
writeStatus
(
pht
(
'CHECKOUT'
)
,
pht
(
'Local branch "%s" has unpublished changes, checking it out '
.
'but leaving them in place.'
,
$dst_branch
)
)
;
$do_reset
=
false
;
}
else
{
$log
->
writeStatus
(
pht
(
'UPDATE'
)
,
pht
(
'Switching to local branch "%s".'
,
$dst_branch
)
)
;
$do_reset
=
true
;
}
$api
->
execxLocal
(
'checkout %s --'
,
$dst_branch
)
;
if
(
$do_reset
)
{
$api
->
execxLocal
(
'reset --hard %s --'
,
$into_commit
)
;
}
$state
->
discardLocalState
(
)
;
return
;
}
$onto_refs
=
array_fuse
(
$this
->
getOntoRefs
(
)
)
;
$pull_branches
=
array
(
)
;
foreach
(
$update_branches
as
$update_branch
)
{
$update_path
=
$api
->
getPathToUpstream
(
$update_branch
)
;
// Remove any branches which contain upstream cycles.
if
(
$update_path
->
getCycle
(
)
)
{
$log
->
writeWarning
(
pht
(
'LOCAL CYCLE'
)
,
pht
(
'Local branch "%s" tracks an upstream but following it leads to '
.
'a local cycle, ignoring branch.'
,
$update_branch
)
)
;
continue
;
}
// Remove any branches not connected to a remote.
if
(
!
$update_path
->
isConnectedToRemote
(
)
)
{
continue
;
}
// Remove any branches connected to a remote other than the remote
// we actually pushed to.
$remote_name
=
$update_path
->
getRemoteRemoteName
(
)
;
if
(
$remote_name
!==
$this
->
getOntoRemote
(
)
)
{
continue
;
}
// Remove any branches not connected to a branch we pushed to.
$remote_branch
=
$update_path
->
getRemoteBranchName
(
)
;
if
(
!
isset
(
$onto_refs
[
$remote_branch
]
)
)
{
continue
;
}
// This is the most-desirable path between some local branch and
// an impacted upstream. Select it and continue.
$pull_branches
=
$update_path
->
getLocalBranches
(
)
;
break
;
}
// When we update these branches later, we want to start with the branch
// closest to the upstream and work our way down.
$pull_branches
=
array_reverse
(
$pull_branches
)
;
$pull_branches
=
array_fuse
(
$pull_branches
)
;
// If we started on a branch and it still exists but is not impacted
// by the changes we made to the remote (i.e., we aren't actually going
// to pull or update it if we continue), just switch back to it now. It's
// okay if this branch is completely unrelated to the changes we just
// landed.
if
(
$local_ref
!==
null
)
{
if
(
isset
(
$update_branches
[
$local_ref
]
)
)
{
if
(
!
isset
(
$pull_branches
[
$local_ref
]
)
)
{
$log
->
writeStatus
(
pht
(
'RETURN'
)
,
pht
(
'Returning to original branch "%s" in original state.'
,
$local_ref
)
)
;
$state
->
restoreLocalState
(
)
;
return
;
}
}
}
// Otherwise, if we don't have any path from the upstream to any local
// branch, we don't want to switch to some unrelated branch which happens
// to have the same name as a branch we interacted with. Just stay where
// we ended up.
$dst_branch
=
null
;
if
(
$pull_branches
)
{
$dst_branch
=
null
;
foreach
(
$pull_branches
as
$pull_branch
)
{
if
(
!
$this
->
isAncestorOf
(
$pull_branch
,
$into_commit
)
)
{
$log
->
writeStatus
(
pht
(
'LOCAL CHANGES'
)
,
pht
(
'Local branch "%s" has unpublished changes, ending updates.'
,
$pull_branch
)
)
;
break
;
}
$log
->
writeStatus
(
pht
(
'UPDATE'
)
,
pht
(
'Updating local branch "%s"...'
,
$pull_branch
)
)
;
$api
->
execxLocal
(
'branch -f %s %s --'
,
$pull_branch
,
$into_commit
)
;
$dst_branch
=
$pull_branch
;
}
}
if
(
$dst_branch
)
{
$log
->
writeStatus
(
pht
(
'CHECKOUT'
)
,
pht
(
'Checking out "%s".'
,
$dst_branch
)
)
;
$api
->
execxLocal
(
'checkout %s --'
,
$dst_branch
)
;
}
else
{
$log
->
writeStatus
(
pht
(
'DETACHED HEAD'
)
,
pht
(
'Unable to find any local branches to update, staying on '
.
'detached head.'
)
)
;
}
$state
->
discardLocalState
(
)
;
}
private
function
isAncestorOf
(
$branch
,
$commit
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
list
(
$stdout
)
=
$api
->
execxLocal
(
'merge-base %s %s'
,
$branch
,
$commit
)
;
$merge_base
=
trim
(
$stdout
)
;
list
(
$stdout
)
=
$api
->
execxLocal
(
'rev-parse --verify %s'
,
$branch
)
;
$branch_hash
=
trim
(
$stdout
)
;
return
(
$merge_base
===
$branch_hash
)
;
}
private
function
getAuthorAndDate
(
$commit
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
list
(
$info
)
=
$api
->
execxLocal
(
'log -n1 --format=%s %s --'
,
'%aD%n%an%n%ae'
,
$commit
)
;
$info
=
trim
(
$info
)
;
list
(
$date
,
$author
,
$email
)
=
explode
(
"\n"
,
$info
,
3
)
;
return
array
(
"$author <{$email}>"
,
$date
,
)
;
}
private
function
didHoldChanges
(
)
{
$log
=
$this
->
getLogEngine
(
)
;
if
(
$this
->
getIsGitPerforce
(
)
)
{
$this
->
writeInfo
(
pht
(
'HOLD'
)
,
pht
(
'Holding change locally, it has not been submitted.'
)
)
;
$push_command
=
csprintf
(
'$ git p4 submit -M --commit %R --'
,
$this
->
mergedRef
)
;
}
else
{
$log
->
writeStatus
(
pht
(
'HOLD'
)
,
pht
(
'Holding change locally, it has not been pushed.'
)
)
;
$push_command
=
csprintf
(
'$ git push -- %R %R:%R'
,
$this
->
getTargetRemote
(
)
,
$this
->
mergedRef
,
$this
->
getTargetOnto
(
)
)
;
}
$restore_command
=
csprintf
(
'$ git checkout %R --'
,
$this
->
localRef
)
;
echo
tsprintf
(
"\n%s\n\n"
.
"%s\n\n"
.
" **%s**\n\n"
.
"%s\n\n"
.
" **%s**\n\n"
.
"%s\n"
,
pht
(
'This local working copy now contains the merged changes in a '
.
'detached state.'
)
,
pht
(
'You can push the changes manually with this command:'
)
,
$push_command
,
pht
(
'You can go back to how things were before you ran "arc land" with '
.
'this command:'
)
,
$restore_command
,
pht
(
'Local branches have not been changed, and are still in exactly the '
.
'same state as before.'
)
)
;
}
protected
function
resolveSymbols
(
array
$symbols
)
{
assert_instances_of
(
$symbols
,
'ArcanistLandSymbol'
)
;
$api
=
$this
->
getRepositoryAPI
(
)
;
foreach
(
$symbols
as
$symbol
)
{
$raw_symbol
=
$symbol
->
getSymbol
(
)
;
list
(
$err
,
$stdout
)
=
$api
->
execManualLocal
(
'rev-parse --verify %s'
,
$raw_symbol
)
;
if
(
$err
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'Branch "%s" does not exist in the local working copy.'
,
$raw_symbol
)
)
;
}
$commit
=
trim
(
$stdout
)
;
$symbol
->
setCommit
(
$commit
)
;
}
}
protected
function
confirmOntoRefs
(
array
$onto_refs
)
{
foreach
(
$onto_refs
as
$onto_ref
)
{
if
(
!
strlen
(
$onto_ref
)
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'Selected "onto" ref "%s" is invalid: the empty string is not '
.
'a valid ref.'
,
$onto_ref
)
)
;
}
}
}
protected
function
selectOntoRefs
(
array
$symbols
)
{
assert_instances_of
(
$symbols
,
'ArcanistLandSymbol'
)
;
$log
=
$this
->
getLogEngine
(
)
;
$onto
=
$this
->
getOntoArguments
(
)
;
if
(
$onto
)
{
$log
->
writeStatus
(
pht
(
'ONTO TARGET'
)
,
pht
(
'Refs were selected with the "--onto" flag: %s.'
,
implode
(
', '
,
$onto
)
)
)
;
return
$onto
;
}
$onto
=
$this
->
getOntoFromConfiguration
(
)
;
if
(
$onto
)
{
$onto_key
=
$this
->
getOntoConfigurationKey
(
)
;
$log
->
writeStatus
(
pht
(
'ONTO TARGET'
)
,
pht
(
'Refs were selected by reading "%s" configuration: %s.'
,
$onto_key
,
implode
(
', '
,
$onto
)
)
)
;
return
$onto
;
}
$api
=
$this
->
getRepositoryAPI
(
)
;
$remote_onto
=
array
(
)
;
foreach
(
$symbols
as
$symbol
)
{
$raw_symbol
=
$symbol
->
getSymbol
(
)
;
$path
=
$api
->
getPathToUpstream
(
$raw_symbol
)
;
if
(
!
$path
->
getLength
(
)
)
{
continue
;
}
$cycle
=
$path
->
getCycle
(
)
;
if
(
$cycle
)
{
$log
->
writeWarning
(
pht
(
'LOCAL CYCLE'
)
,
pht
(
'Local branch "%s" tracks an upstream, but following it leads '
.
'to a local cycle; ignoring branch upstream.'
,
$raw_symbol
)
)
;
$log
->
writeWarning
(
pht
(
'LOCAL CYCLE'
)
,
implode
(
' -> '
,
$cycle
)
)
;
continue
;
}
if
(
!
$path
->
isConnectedToRemote
(
)
)
{
$log
->
writeWarning
(
pht
(
'NO PATH TO REMOTE'
)
,
pht
(
'Local branch "%s" tracks an upstream, but there is no path '
.
'to a remote; ignoring branch upstream.'
,
$raw_symbol
)
)
;
continue
;
}
$onto
=
$path
->
getRemoteBranchName
(
)
;
$remote_onto
[
$onto
]
=
$onto
;
}
if
(
count
(
$remote_onto
)
>
1
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'The branches you are landing are connected to multiple different '
.
'remote branches via Git branch upstreams. Use "--onto" to select '
.
'the refs you want to push to.'
)
)
;
}
if
(
$remote_onto
)
{
$remote_onto
=
array_values
(
$remote_onto
)
;
$log
->
writeStatus
(
pht
(
'ONTO TARGET'
)
,
pht
(
'Landing onto target "%s", selected by following tracking branches '
.
'upstream to the closest remote branch.'
,
head
(
$remote_onto
)
)
)
;
return
$remote_onto
;
}
$default_onto
=
'master'
;
$log
->
writeStatus
(
pht
(
'ONTO TARGET'
)
,
pht
(
'Landing onto target "%s", the default target under Git.'
,
$default_onto
)
)
;
return
array
(
$default_onto
)
;
}
protected
function
selectOntoRemote
(
array
$symbols
)
{
assert_instances_of
(
$symbols
,
'ArcanistLandSymbol'
)
;
$remote
=
$this
->
newOntoRemote
(
$symbols
)
;
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
$is_pushable
=
$api
->
isPushableRemote
(
$remote
)
;
$is_perforce
=
$api
->
isPerforceRemote
(
$remote
)
;
if
(
!
$is_pushable
&&
!
$is_perforce
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'No pushable remote "%s" exists. Use the "--onto-remote" flag to '
.
'choose a valid, pushable remote to land changes onto.'
,
$remote
)
)
;
}
if
(
$is_perforce
)
{
$this
->
setIsGitPerforce
(
true
)
;
$log
->
writeWarning
(
pht
(
'P4 MODE'
)
,
pht
(
'Operating in Git/Perforce mode after selecting a Perforce '
.
'remote.'
)
)
;
if
(
!
$this
->
isSquashStrategy
(
)
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'Perforce mode does not support the "merge" land strategy. '
.
'Use the "squash" land strategy when landing to a Perforce '
.
'remote (you can use "--squash" to select this strategy).'
)
)
;
}
}
return
$remote
;
}
private
function
newOntoRemote
(
array
$onto_symbols
)
{
assert_instances_of
(
$onto_symbols
,
'ArcanistLandSymbol'
)
;
$log
=
$this
->
getLogEngine
(
)
;
$remote
=
$this
->
getOntoRemoteArgument
(
)
;
if
(
$remote
!==
null
)
{
$log
->
writeStatus
(
pht
(
'ONTO REMOTE'
)
,
pht
(
'Remote "%s" was selected with the "--onto-remote" flag.'
,
$remote
)
)
;
return
$remote
;
}
$remote
=
$this
->
getOntoRemoteFromConfiguration
(
)
;
if
(
$remote
!==
null
)
{
$remote_key
=
$this
->
getOntoRemoteConfigurationKey
(
)
;
$log
->
writeStatus
(
pht
(
'ONTO REMOTE'
)
,
pht
(
'Remote "%s" was selected by reading "%s" configuration.'
,
$remote
,
$remote_key
)
)
;
return
$remote
;
}
$api
=
$this
->
getRepositoryAPI
(
)
;
$upstream_remotes
=
array
(
)
;
foreach
(
$onto_symbols
as
$onto_symbol
)
{
$path
=
$api
->
getPathToUpstream
(
$onto_symbol
->
getSymbol
(
)
)
;
$remote
=
$path
->
getRemoteRemoteName
(
)
;
if
(
$remote
!==
null
)
{
$upstream_remotes
[
$remote
]
[
]
=
$onto_symbol
;
}
}
if
(
count
(
$upstream_remotes
)
>
1
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'The "onto" refs you have selected are connected to multiple '
.
'different remotes via Git branch upstreams. Use "--onto-remote" '
.
'to select a single remote.'
)
)
;
}
if
(
$upstream_remotes
)
{
$upstream_remote
=
head_key
(
$upstream_remotes
)
;
$log
->
writeStatus
(
pht
(
'ONTO REMOTE'
)
,
pht
(
'Remote "%s" was selected by following tracking branches '
.
'upstream to the closest remote.'
,
$remote
)
)
;
return
$upstream_remote
;
}
$perforce_remote
=
'p4'
;
if
(
$api
->
isPerforceRemote
(
$remote
)
)
{
$log
->
writeStatus
(
pht
(
'ONTO REMOTE'
)
,
pht
(
'Peforce remote "%s" was selected because the existence of '
.
'this remote implies this working copy was synchronized '
.
'from a Perforce repository.'
,
$remote
)
)
;
return
$remote
;
}
$default_remote
=
'origin'
;
$log
->
writeStatus
(
pht
(
'ONTO REMOTE'
)
,
pht
(
'Landing onto remote "%s", the default remote under Git.'
,
$default_remote
)
)
;
return
$default_remote
;
}
protected
function
selectIntoRemote
(
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
if
(
$this
->
getIntoEmptyArgument
(
)
)
{
$this
->
setIntoEmpty
(
true
)
;
$log
->
writeStatus
(
pht
(
'INTO REMOTE'
)
,
pht
(
'Will merge into empty state, selected with the "--into-empty" '
.
'flag.'
)
)
;
return
;
}
if
(
$this
->
getIntoLocalArgument
(
)
)
{
$this
->
setIntoLocal
(
true
)
;
$log
->
writeStatus
(
pht
(
'INTO REMOTE'
)
,
pht
(
'Will merge into local state, selected with the "--into-local" '
.
'flag.'
)
)
;
return
;
}
$into
=
$this
->
getIntoRemoteArgument
(
)
;
if
(
$into
!==
null
)
{
// TODO: We could allow users to pass a URI argument instead, but
// this also requires some updates to the fetch logic elsewhere.
if
(
!
$api
->
isFetchableRemote
(
$into
)
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'Remote "%s", specified with "--into", is not a valid fetchable '
.
'remote.'
,
$into
)
)
;
}
$this
->
setIntoRemote
(
$into
)
;
$log
->
writeStatus
(
pht
(
'INTO REMOTE'
)
,
pht
(
'Will merge into remote "%s", selected with the "--into" flag.'
,
$into
)
)
;
return
;
}
$onto
=
$this
->
getOntoRemote
(
)
;
$this
->
setIntoRemote
(
$onto
)
;
$log
->
writeStatus
(
pht
(
'INTO REMOTE'
)
,
pht
(
'Will merge into remote "%s" by default, because this is the remote '
.
'the change is landing onto.'
,
$onto
)
)
;
}
protected
function
selectIntoRef
(
)
{
$log
=
$this
->
getLogEngine
(
)
;
if
(
$this
->
getIntoEmptyArgument
(
)
)
{
$log
->
writeStatus
(
pht
(
'INTO TARGET'
)
,
pht
(
'Will merge into empty state, selected with the "--into-empty" '
.
'flag.'
)
)
;
return
;
}
$into
=
$this
->
getIntoArgument
(
)
;
if
(
$into
!==
null
)
{
$this
->
setIntoRef
(
$into
)
;
$log
->
writeStatus
(
pht
(
'INTO TARGET'
)
,
pht
(
'Will merge into target "%s", selected with the "--into" flag.'
,
$into
)
)
;
return
;
}
$ontos
=
$this
->
getOntoRefs
(
)
;
$onto
=
head
(
$ontos
)
;
$this
->
setIntoRef
(
$onto
)
;
if
(
count
(
$ontos
)
>
1
)
{
$log
->
writeStatus
(
pht
(
'INTO TARGET'
)
,
pht
(
'Will merge into target "%s" by default, because this is the first '
.
'"onto" target.'
,
$onto
)
)
;
}
else
{
$log
->
writeStatus
(
pht
(
'INTO TARGET'
)
,
pht
(
'Will merge into target "%s" by default, because this is the "onto" '
.
'target.'
,
$onto
)
)
;
}
}
protected
function
selectIntoCommit
(
)
{
// Make sure that our "into" target is valid.
$log
=
$this
->
getLogEngine
(
)
;
if
(
$this
->
getIntoEmpty
(
)
)
{
// If we're running under "--into-empty", we don't have to do anything.
$log
->
writeStatus
(
pht
(
'INTO COMMIT'
)
,
pht
(
'Preparing merge into the empty state.'
)
)
;
return
null
;
}
if
(
$this
->
getIntoLocal
(
)
)
{
// If we're running under "--into-local", just make sure that the
// target identifies some actual commit.
$api
=
$this
->
getRepositoryAPI
(
)
;
$local_ref
=
$this
->
getIntoRef
(
)
;
list
(
$err
,
$stdout
)
=
$api
->
execManualLocal
(
'rev-parse --verify %s'
,
$local_ref
)
;
if
(
$err
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'Local ref "%s" does not exist.'
,
$local_ref
)
)
;
}
$into_commit
=
trim
(
$stdout
)
;
$log
->
writeStatus
(
pht
(
'INTO COMMIT'
)
,
pht
(
'Preparing merge into local target "%s", at commit "%s".'
,
$local_ref
,
$this
->
getDisplayHash
(
$into_commit
)
)
)
;
return
$into_commit
;
}
$target
=
id
(
new
ArcanistLandTarget
(
)
)
->
setRemote
(
$this
->
getIntoRemote
(
)
)
->
setRef
(
$this
->
getIntoRef
(
)
)
;
$commit
=
$this
->
fetchTarget
(
$target
)
;
if
(
$commit
!==
null
)
{
$log
->
writeStatus
(
pht
(
'INTO COMMIT'
)
,
pht
(
'Preparing merge into "%s" from remote "%s", at commit "%s".'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
,
$this
->
getDisplayHash
(
$commit
)
)
)
;
return
$commit
;
}
// If we have no valid target and the user passed "--into" explicitly,
// treat this as an error. For example, "arc land --into Q --onto Q",
// where "Q" does not exist, is an error.
if
(
$this
->
getIntoArgument
(
)
)
{
throw
new
PhutilArgumentUsageException
(
pht
(
'Ref "%s" does not exist in remote "%s".'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
}
// Otherwise, treat this as implying "--into-empty". For example,
// "arc land --onto Q", where "Q" does not exist, is equivalent to
// "arc land --into-empty --onto Q".
$this
->
setIntoEmpty
(
true
)
;
$log
->
writeStatus
(
pht
(
'INTO COMMIT'
)
,
pht
(
'Preparing merge into the empty state to create target "%s" '
.
'in remote "%s".'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
return
null
;
}
private
function
getLandTargetLocalCommit
(
ArcanistLandTarget
$target
)
{
$commit
=
$this
->
resolveLandTargetLocalCommit
(
$target
)
;
if
(
$commit
===
null
)
{
throw
new
Exception
(
pht
(
'No ref "%s" exists in remote "%s".'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
}
return
$commit
;
}
private
function
getLandTargetLocalExists
(
ArcanistLandTarget
$target
)
{
$commit
=
$this
->
resolveLandTargetLocalCommit
(
$target
)
;
return
(
$commit
!==
null
)
;
}
private
function
resolveLandTargetLocalCommit
(
ArcanistLandTarget
$target
)
{
$target_key
=
$target
->
getLandTargetKey
(
)
;
if
(
!
array_key_exists
(
$target_key
,
$this
->
landTargetCommitMap
)
)
{
$full_ref
=
sprintf
(
'refs/remotes/%s/%s'
,
$target
->
getRemote
(
)
,
$target
->
getRef
(
)
)
;
$api
=
$this
->
getRepositoryAPI
(
)
;
list
(
$err
,
$stdout
)
=
$api
->
execManualLocal
(
'rev-parse --verify %s'
,
$full_ref
)
;
if
(
$err
)
{
$result
=
null
;
}
else
{
$result
=
trim
(
$stdout
)
;
}
$this
->
landTargetCommitMap
[
$target_key
]
=
$result
;
}
return
$this
->
landTargetCommitMap
[
$target_key
]
;
}
private
function
fetchLandTarget
(
ArcanistLandTarget
$target
,
$ignore_failure
=
false
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
// TODO: Format this fetch nicely as a workflow command.
$err
=
$api
->
execPassthru
(
'fetch --no-tags --quiet -- %s %s'
,
$target
->
getRemote
(
)
,
$target
->
getRef
(
)
)
;
if
(
$err
&&
!
$ignore_failure
)
{
throw
new
ArcanistUsageException
(
pht
(
'Fetch of "%s" from remote "%s" failed! Fix the error and '
.
'run "arc land" again.'
,
$target
->
getRef
(
)
,
$target
->
getRemote
(
)
)
)
;
}
// TODO: If the remote is a bare URI, we could read ".git/FETCH_HEAD"
// here and write the commit into the map. For now, settle for clearing
// the cache.
// We could also fetch into some named "refs/arc-land-temporary" named
// ref, then read that.
if
(
!
$err
)
{
$target_key
=
$target
->
getLandTargetKey
(
)
;
unset
(
$this
->
landTargetCommitMap
[
$target_key
]
)
;
}
}
protected
function
selectCommits
(
$into_commit
,
array
$symbols
)
{
assert_instances_of
(
$symbols
,
'ArcanistLandSymbol'
)
;
$api
=
$this
->
getRepositoryAPI
(
)
;
$commit_map
=
array
(
)
;
foreach
(
$symbols
as
$symbol
)
{
$symbol_commit
=
$symbol
->
getCommit
(
)
;
$format
=
'%H%x00%P%x00%s%x00'
;
if
(
$into_commit
===
null
)
{
list
(
$commits
)
=
$api
->
execxLocal
(
'log %s --format=%s'
,
$symbol_commit
,
$format
)
;
}
else
{
list
(
$commits
)
=
$api
->
execxLocal
(
'log %s --not %s --format=%s'
,
$symbol_commit
,
$into_commit
,
$format
)
;
}
$commits
=
phutil_split_lines
(
$commits
,
false
)
;
$is_first
=
false
;
foreach
(
$commits
as
$line
)
{
if
(
!
strlen
(
$line
)
)
{
continue
;
}
$parts
=
explode
(
"\0"
,
$line
,
4
)
;
if
(
count
(
$parts
)
<
3
)
{
throw
new
Exception
(
pht
(
'Unexpected output from "git log ...": %s'
,
$line
)
)
;
}
$hash
=
$parts
[
0
]
;
if
(
!
isset
(
$commit_map
[
$hash
]
)
)
{
$parents
=
$parts
[
1
]
;
$parents
=
trim
(
$parents
)
;
if
(
strlen
(
$parents
)
)
{
$parents
=
explode
(
' '
,
$parents
)
;
}
else
{
$parents
=
array
(
)
;
}
$summary
=
$parts
[
2
]
;
$commit_map
[
$hash
]
=
id
(
new
ArcanistLandCommit
(
)
)
->
setHash
(
$hash
)
->
setParents
(
$parents
)
->
setSummary
(
$summary
)
;
}
$commit
=
$commit_map
[
$hash
]
;
if
(
$is_first
)
{
$commit
->
addDirectSymbol
(
$symbol
)
;
$is_first
=
false
;
}
$commit
->
addIndirectSymbol
(
$symbol
)
;
}
}
return
$this
->
confirmCommits
(
$into_commit
,
$symbols
,
$commit_map
)
;
}
protected
function
getDefaultSymbols
(
)
{
$api
=
$this
->
getRepositoryAPI
(
)
;
$log
=
$this
->
getLogEngine
(
)
;
$branch
=
$api
->
getBranchName
(
)
;
if
(
$branch
===
null
)
{
$log
->
writeStatus
(
pht
(
'SOURCE'
)
,
pht
(
'Landing the current branch, "%s".'
,
$branch
)
)
;
return
array
(
$branch
)
;
}
$commit
=
$api
->
getCurrentCommitRef
(
)
;
$log
->
writeStatus
(
pht
(
'SOURCE'
)
,
pht
(
'Landing the current HEAD, "%s".'
,
$commit
->
getCommitHash
(
)
)
)
;
return
array
(
$branch
)
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jan 19, 17:58 (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127201
Default Alt Text
ArcanistGitLandEngine.php (38 KB)
Attached To
Mode
rARC Arcanist
Attached
Detach File
Event Timeline
Log In to Comment