Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F969280
ArcanistLandWorkflow.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
49 KB
Referenced Files
None
Subscribers
None
ArcanistLandWorkflow.php
View Options
<?php
/**
* Lands a branch by rebasing, merging and amending it.
*/
final
class
ArcanistLandWorkflow
extends
ArcanistWorkflow
{
private
$isGit
;
private
$isGitSvn
;
private
$isHg
;
private
$isHgSvn
;
private
$oldBranch
;
private
$branch
;
private
$onto
;
private
$ontoRemoteBranch
;
private
$remote
;
private
$useSquash
;
private
$keepBranch
;
private
$branchType
;
private
$ontoType
;
private
$preview
;
private
$revision
;
private
$messageFile
;
const
REFTYPE_BRANCH
=
'branch'
;
const
REFTYPE_BOOKMARK
=
'bookmark'
;
public
function
getRevisionDict
()
{
return
$this
->
revision
;
}
public
function
getWorkflowName
()
{
return
'land'
;
}
public
function
getCommandSynopses
()
{
return
phutil_console_format
(
<<<EOTEXT
**land** [__options__] [__ref__]
EOTEXT
);
}
public
function
getCommandHelp
()
{
return
phutil_console_format
(
<<<EOTEXT
Supports: git, git/p4, hg
Publish an accepted revision after review. This command is the last
step in the standard Differential code review workflow.
This command merges and pushes changes associated with an accepted
revision that are currently sitting in __ref__, which is usually the
name of a local branch. Without __ref__, the current working copy
state will be used.
Under Git: branches, tags, and arbitrary commits (detached HEADs)
may be landed.
Under Git/Perforce: branches, tags, and arbitrary commits may
be submitted.
Under Mercurial: branches and bookmarks may be landed, but only
onto a target of the same type. See T3855.
The workflow selects a target branch to land onto and a remote where
the change will be pushed to.
A target branch is selected by examining these sources in order:
- the **--onto** flag;
- the upstream of the branch targeted by the land operation,
recursively (Git only);
- the __arc.land.onto.default__ configuration setting;
- or by falling back to a standard default:
- "master" in Git;
- "default" in Mercurial.
A remote is selected by examining these sources in order:
- the **--remote** flag;
- the upstream of the current branch, recursively (Git only);
- the special "p4" remote which indicates a repository has
been synchronized with Perforce (Git only);
- or by falling back to a standard default:
- "origin" in Git;
- the default remote in Mercurial.
After selecting a target branch and a remote, the commits which will
be landed are printed.
With **--preview**, execution stops here, before the change is
merged.
The change is merged with the changes in the target branch,
following these rules:
In repositories with mutable history or with **--squash**, this will
perform a squash merge (the entire branch will be represented as one
commit after the merge).
In repositories with immutable history or with **--merge**, this will
perform a strict merge (a merge commit will always be created, and
local commits will be preserved).
The resulting commit will be given an up-to-date commit message
describing the final state of the revision in Differential.
In Git, the merge occurs in a detached HEAD. The local branch
reference (if one exists) is not updated yet.
With **--hold**, execution stops here, before the change is pushed.
The change is pushed into the remote.
Consulting mystical sources of power, the workflow makes a guess
about what state you wanted to end up in after the process finishes
and the working copy is put into that state.
The branch which was landed is deleted, unless the **--keep-branch**
flag was passed or the landing branch is the same as the target
branch.
EOTEXT
);
}
public
function
requiresWorkingCopy
()
{
return
true
;
}
public
function
requiresConduit
()
{
return
true
;
}
public
function
requiresAuthentication
()
{
return
true
;
}
public
function
requiresRepositoryAPI
()
{
return
true
;
}
public
function
getArguments
()
{
return
array
(
'onto'
=>
array
(
'param'
=>
'master'
,
'help'
=>
pht
(
"Land feature branch onto a branch other than the default "
.
"('master' in git, 'default' in hg). You can change the default "
.
"by setting '%s' with `%s` or for the entire project in %s."
,
'arc.land.onto.default'
,
'arc set-config'
,
'.arcconfig'
),
),
'hold'
=>
array
(
'help'
=>
pht
(
'Prepare the change to be pushed, but do not actually push it.'
),
),
'keep-branch'
=>
array
(
'help'
=>
pht
(
'Keep the feature branch after pushing changes to the '
.
'remote (by default, it is deleted).'
),
),
'remote'
=>
array
(
'param'
=>
'origin'
,
'help'
=>
pht
(
'Push to a remote other than the default.'
),
),
'merge'
=>
array
(
'help'
=>
pht
(
'Perform a %s merge, not a %s merge. If the project '
.
'is marked as having an immutable history, this is the default '
.
'behavior.'
,
'--no-ff'
,
'--squash'
),
'supports'
=>
array
(
'git'
,
),
'nosupport'
=>
array
(
'hg'
=>
pht
(
'Use the %s strategy when landing in mercurial.'
,
'--squash'
),
),
),
'squash'
=>
array
(
'help'
=>
pht
(
'Perform a %s merge, not a %s merge. If the project is '
.
'marked as having a mutable history, this is the default behavior.'
,
'--squash'
,
'--no-ff'
),
'conflicts'
=>
array
(
'merge'
=>
pht
(
'%s and %s are conflicting merge strategies.'
,
'--merge'
,
'--squash'
),
),
),
'delete-remote'
=>
array
(
'help'
=>
pht
(
'Delete the feature branch in the remote after landing it.'
),
'conflicts'
=>
array
(
'keep-branch'
=>
true
,
),
'supports'
=>
array
(
'hg'
,
),
),
'revision'
=>
array
(
'param'
=>
'id'
,
'help'
=>
pht
(
'Use the message from a specific revision, rather than '
.
'inferring the revision based on branch content.'
),
),
'preview'
=>
array
(
'help'
=>
pht
(
'Prints the commits that would be landed. Does not '
.
'actually modify or land the commits.'
),
),
'*'
=>
'branch'
,
);
}
public
function
run
()
{
$this
->
readArguments
();
$engine
=
null
;
if
(
$this
->
isGit
&&
!
$this
->
isGitSvn
)
{
$engine
=
new
ArcanistGitLandEngine
();
}
if
(
$engine
)
{
$should_hold
=
$this
->
getArgument
(
'hold'
);
$remote_arg
=
$this
->
getArgument
(
'remote'
);
$onto_arg
=
$this
->
getArgument
(
'onto'
);
$engine
->
setWorkflow
(
$this
)
->
setRepositoryAPI
(
$this
->
getRepositoryAPI
())
->
setSourceRef
(
$this
->
branch
)
->
setShouldHold
(
$should_hold
)
->
setShouldKeep
(
$this
->
keepBranch
)
->
setShouldSquash
(
$this
->
useSquash
)
->
setShouldPreview
(
$this
->
preview
)
->
setRemoteArgument
(
$remote_arg
)
->
setOntoArgument
(
$onto_arg
)
->
setBuildMessageCallback
(
array
(
$this
,
'buildEngineMessage'
));
// The goal here is to raise errors with flags early (which is cheap),
// before we test if the working copy is clean (which can be slow). This
// could probably be structured more cleanly.
$engine
->
parseArguments
();
// This must be configured or we fail later inside "buildEngineMessage()".
// This is less than ideal.
$this
->
ontoRemoteBranch
=
sprintf
(
'%s/%s'
,
$engine
->
getTargetRemote
(),
$engine
->
getTargetOnto
());
$this
->
requireCleanWorkingCopy
();
$engine
->
execute
();
if
(!
$should_hold
&&
!
$this
->
preview
)
{
$this
->
didPush
();
}
return
0
;
}
$this
->
validate
();
try
{
$this
->
pullFromRemote
();
}
catch
(
Exception
$ex
)
{
$this
->
restoreBranch
();
throw
$ex
;
}
$this
->
printPendingCommits
();
if
(
$this
->
preview
)
{
$this
->
restoreBranch
();
return
0
;
}
$this
->
checkoutBranch
();
$this
->
findRevision
();
if
(
$this
->
useSquash
)
{
$this
->
rebase
();
$this
->
squash
();
}
else
{
$this
->
merge
();
}
$this
->
push
();
if
(!
$this
->
keepBranch
)
{
$this
->
cleanupBranch
();
}
if
(
$this
->
oldBranch
!=
$this
->
onto
)
{
// If we were on some branch A and the user ran "arc land B",
// switch back to A.
if
(
$this
->
keepBranch
||
$this
->
oldBranch
!=
$this
->
branch
)
{
$this
->
restoreBranch
();
}
}
echo
pht
(
'Done.'
),
"
\n
"
;
return
0
;
}
private
function
getUpstreamMatching
(
$branch
,
$pattern
)
{
if
(
$this
->
isGit
)
{
$repository_api
=
$this
->
getRepositoryAPI
();
list
(
$err
,
$fullname
)
=
$repository_api
->
execManualLocal
(
'rev-parse --symbolic-full-name %s@{upstream}'
,
$branch
);
if
(!
$err
)
{
$matches
=
null
;
if
(
preg_match
(
$pattern
,
$fullname
,
$matches
))
{
return
last
(
$matches
);
}
}
}
return
null
;
}
private
function
getGitSvnTrunk
()
{
if
(!
$this
->
isGitSvn
)
{
return
null
;
}
// See T13293, this depends on the options passed when cloning.
// On any error we return `trunk`, which was the previous default.
$repository_api
=
$this
->
getRepositoryAPI
();
list
(
$err
,
$refspec
)
=
$repository_api
->
execManualLocal
(
'config svn-remote.svn.fetch'
);
if
(
$err
)
{
return
'trunk'
;
}
$refspec
=
rtrim
(
substr
(
$refspec
,
strrpos
(
$refspec
,
':'
)
+
1
));
$prefix
=
'refs/remotes/'
;
if
(
substr
(
$refspec
,
0
,
strlen
(
$prefix
))
!==
$prefix
)
{
return
'trunk'
;
}
$refspec
=
substr
(
$refspec
,
strlen
(
$prefix
));
return
$refspec
;
}
private
function
readArguments
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
$this
->
isGit
=
$repository_api
instanceof
ArcanistGitAPI
;
$this
->
isHg
=
$repository_api
instanceof
ArcanistMercurialAPI
;
if
(
$this
->
isGit
)
{
$repository
=
$this
->
loadProjectRepository
();
$this
->
isGitSvn
=
(
idx
(
$repository
,
'vcs'
)
==
'svn'
);
}
if
(
$this
->
isHg
)
{
$this
->
isHgSvn
=
$repository_api
->
isHgSubversionRepo
();
}
$branch
=
$this
->
getArgument
(
'branch'
);
if
(
empty
(
$branch
))
{
$branch
=
$this
->
getBranchOrBookmark
();
if
(
$branch
!==
null
)
{
$this
->
branchType
=
$this
->
getBranchType
(
$branch
);
// TODO: This message is misleading when landing a detached head or
// a tag in Git.
echo
pht
(
"Landing current %s '%s'."
,
$this
->
branchType
,
$branch
),
"
\n
"
;
$branch
=
array
(
$branch
);
}
}
if
(
count
(
$branch
)
!==
1
)
{
throw
new
ArcanistUsageException
(
pht
(
'Specify exactly one branch or bookmark to land changes from.'
));
}
$this
->
branch
=
head
(
$branch
);
$this
->
keepBranch
=
$this
->
getArgument
(
'keep-branch'
);
$this
->
preview
=
$this
->
getArgument
(
'preview'
);
if
(!
$this
->
branchType
)
{
$this
->
branchType
=
$this
->
getBranchType
(
$this
->
branch
);
}
$onto_default
=
$this
->
isGit
?
'master'
:
'default'
;
$onto_default
=
nonempty
(
$this
->
getConfigFromAnySource
(
'arc.land.onto.default'
),
$onto_default
);
$onto_default
=
coalesce
(
$this
->
getUpstreamMatching
(
$this
->
branch
,
'/^refs
\/
heads
\/
(.+)$/'
),
$onto_default
);
$this
->
onto
=
$this
->
getArgument
(
'onto'
,
$onto_default
);
$this
->
ontoType
=
$this
->
getBranchType
(
$this
->
onto
);
$remote_default
=
$this
->
isGit
?
'origin'
:
''
;
$remote_default
=
coalesce
(
$this
->
getUpstreamMatching
(
$this
->
onto
,
'/^refs
\/
remotes
\/
(.+?)
\/
/'
),
$remote_default
);
$this
->
remote
=
$this
->
getArgument
(
'remote'
,
$remote_default
);
if
(
$this
->
getArgument
(
'merge'
))
{
$this
->
useSquash
=
false
;
}
else
if
(
$this
->
getArgument
(
'squash'
))
{
$this
->
useSquash
=
true
;
}
else
{
$this
->
useSquash
=
!
$this
->
isHistoryImmutable
();
}
$this
->
ontoRemoteBranch
=
$this
->
onto
;
if
(
$this
->
isGitSvn
)
{
$this
->
ontoRemoteBranch
=
$this
->
getGitSvnTrunk
();
}
else
if
(
$this
->
isGit
)
{
$this
->
ontoRemoteBranch
=
$this
->
remote
.
'/'
.
$this
->
onto
;
}
$this
->
oldBranch
=
$this
->
getBranchOrBookmark
();
}
private
function
validate
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$this
->
onto
==
$this
->
branch
)
{
$message
=
pht
(
"You can not land a %s onto itself -- you are trying "
.
"to land '%s' onto '%s'. For more information on how to push "
.
"changes, see 'Pushing and Closing Revisions' in 'Arcanist User "
.
"Guide: arc diff' in the documentation."
,
$this
->
branchType
,
$this
->
branch
,
$this
->
onto
);
if
(!
$this
->
isHistoryImmutable
())
{
$message
.=
' '
.
pht
(
"You may be able to '%s' instead."
,
'arc amend'
);
}
throw
new
ArcanistUsageException
(
$message
);
}
if
(
$this
->
isHg
)
{
if
(
$this
->
useSquash
)
{
if
(!
$repository_api
->
supportsRebase
())
{
throw
new
ArcanistUsageException
(
pht
(
'You must enable the rebase extension to use the %s strategy.'
,
'--squash'
));
}
}
if
(
$this
->
branchType
!=
$this
->
ontoType
)
{
throw
new
ArcanistUsageException
(
pht
(
'Source %s is a %s but destination %s is a %s. When landing a '
.
'%s, the destination must also be a %s. Use %s to specify a %s, '
.
'or set %s in %s.'
,
$this
->
branch
,
$this
->
branchType
,
$this
->
onto
,
$this
->
ontoType
,
$this
->
branchType
,
$this
->
branchType
,
'--onto'
,
$this
->
branchType
,
'arc.land.onto.default'
,
'.arcconfig'
));
}
}
if
(
$this
->
isGit
)
{
list
(
$err
)
=
$repository_api
->
execManualLocal
(
'rev-parse --verify %s'
,
$this
->
branch
);
if
(
$err
)
{
throw
new
ArcanistUsageException
(
pht
(
"Branch '%s' does not exist."
,
$this
->
branch
));
}
}
$this
->
requireCleanWorkingCopy
();
}
private
function
checkoutBranch
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$this
->
getBranchOrBookmark
()
!=
$this
->
branch
)
{
$repository_api
->
execxLocal
(
'checkout %s'
,
$this
->
branch
);
}
switch
(
$this
->
branchType
)
{
case
self
::
REFTYPE_BOOKMARK
:
$message
=
pht
(
'Switched to bookmark **%s**. Identifying and merging...'
,
$this
->
branch
);
break
;
case
self
::
REFTYPE_BRANCH
:
default
:
$message
=
pht
(
'Switched to branch **%s**. Identifying and merging...'
,
$this
->
branch
);
break
;
}
echo
phutil_console_format
(
$message
.
"
\n
"
);
}
private
function
printPendingCommits
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$repository_api
instanceof
ArcanistGitAPI
)
{
list
(
$out
)
=
$repository_api
->
execxLocal
(
'log --oneline %s %s --'
,
$this
->
branch
,
'^'
.
$this
->
onto
);
}
else
if
(
$repository_api
instanceof
ArcanistMercurialAPI
)
{
$common_ancestor
=
$repository_api
->
getCanonicalRevisionName
(
hgsprintf
(
'ancestor(%s,%s)'
,
$this
->
onto
,
$this
->
branch
));
$branch_range
=
hgsprintf
(
'reverse((%s::%s) - %s)'
,
$common_ancestor
,
$this
->
branch
,
$common_ancestor
);
list
(
$out
)
=
$repository_api
->
execxLocal
(
'log -r %s --template %s'
,
$branch_range
,
'{node|short} {desc|firstline}
\n
'
);
}
if
(!
trim
(
$out
))
{
$this
->
restoreBranch
();
throw
new
ArcanistUsageException
(
pht
(
'No commits to land from %s.'
,
$this
->
branch
));
}
echo
pht
(
"The following commit(s) will be landed:
\n\n
%s"
,
$out
),
"
\n
"
;
}
private
function
findRevision
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
$this
->
parseBaseCommitArgument
(
array
(
$this
->
ontoRemoteBranch
));
$revision_id
=
$this
->
getArgument
(
'revision'
);
if
(
$revision_id
)
{
$revision_id
=
$this
->
normalizeRevisionID
(
$revision_id
);
$revisions
=
$this
->
getConduit
()->
callMethodSynchronous
(
'differential.query'
,
array
(
'ids'
=>
array
(
$revision_id
),
));
if
(!
$revisions
)
{
throw
new
ArcanistUsageException
(
pht
(
"No such revision '%s'!"
,
"D{$revision_id}"
));
}
}
else
{
$revisions
=
$repository_api
->
loadWorkingCopyDifferentialRevisions
(
$this
->
getConduit
(),
array
());
}
if
(!
count
(
$revisions
))
{
throw
new
ArcanistUsageException
(
pht
(
"arc can not identify which revision exists on %s '%s'. Update the "
.
"revision with recent changes to synchronize the %s name and hashes, "
.
"or use '%s' to amend the commit message at HEAD, or use "
.
"'%s' to select a revision explicitly."
,
$this
->
branchType
,
$this
->
branch
,
$this
->
branchType
,
'arc amend'
,
'--revision <id>'
));
}
else
if
(
count
(
$revisions
)
>
1
)
{
switch
(
$this
->
branchType
)
{
case
self
::
REFTYPE_BOOKMARK
:
$message
=
pht
(
"There are multiple revisions on feature bookmark '%s' which are "
.
"not present on '%s':
\n\n
"
.
"%s
\n
"
.
'Separate these revisions onto different bookmarks, or use '
.
'--revision <id> to use the commit message from <id> '
.
'and land them all.'
,
$this
->
branch
,
$this
->
onto
,
$this
->
renderRevisionList
(
$revisions
));
break
;
case
self
::
REFTYPE_BRANCH
:
default
:
$message
=
pht
(
"There are multiple revisions on feature branch '%s' which are "
.
"not present on '%s':
\n\n
"
.
"%s
\n
"
.
'Separate these revisions onto different branches, or use '
.
'--revision <id> to use the commit message from <id> '
.
'and land them all.'
,
$this
->
branch
,
$this
->
onto
,
$this
->
renderRevisionList
(
$revisions
));
break
;
}
throw
new
ArcanistUsageException
(
$message
);
}
$this
->
revision
=
head
(
$revisions
);
$rev_status
=
$this
->
revision
[
'status'
];
$rev_id
=
$this
->
revision
[
'id'
];
$rev_title
=
$this
->
revision
[
'title'
];
$rev_auxiliary
=
idx
(
$this
->
revision
,
'auxiliary'
,
array
());
$full_name
=
pht
(
'D%d: %s'
,
$rev_id
,
$rev_title
);
if
(
$this
->
revision
[
'authorPHID'
]
!=
$this
->
getUserPHID
())
{
$other_author
=
$this
->
getConduit
()->
callMethodSynchronous
(
'user.query'
,
array
(
'phids'
=>
array
(
$this
->
revision
[
'authorPHID'
]),
));
$other_author
=
ipull
(
$other_author
,
'userName'
,
'phid'
);
$other_author
=
$other_author
[
$this
->
revision
[
'authorPHID'
]];
$ok
=
phutil_console_confirm
(
pht
(
"This %s has revision '%s' but you are not the author. Land this "
.
"revision by %s?"
,
$this
->
branchType
,
$full_name
,
$other_author
));
if
(!
$ok
)
{
throw
new
ArcanistUserAbortException
();
}
}
$state_warning
=
null
;
$state_header
=
null
;
if
(
$rev_status
==
ArcanistDifferentialRevisionStatus
::
CHANGES_PLANNED
)
{
$state_header
=
pht
(
'REVISION HAS CHANGES PLANNED'
);
$state_warning
=
pht
(
'The revision you are landing ("%s") is currently in the "%s" state, '
.
'indicating that you expect to revise it before moving forward.'
.
"
\n\n
"
.
'Normally, you should resubmit it for review and wait until it is '
.
'"%s" by reviewers before you continue.'
.
"
\n\n
"
.
'To resubmit the revision for review, either: update the revision '
.
'with revised changes; or use "Request Review" from the web interface.'
,
$full_name
,
pht
(
'Changes Planned'
),
pht
(
'Accepted'
));
}
else
if
(
$rev_status
!=
ArcanistDifferentialRevisionStatus
::
ACCEPTED
)
{
$state_header
=
pht
(
'REVISION HAS NOT BEEN ACCEPTED'
);
$state_warning
=
pht
(
'The revision you are landing ("%s") has not been "%s" by reviewers.'
,
$full_name
,
pht
(
'Accepted'
));
}
if
(
$state_warning
!==
null
)
{
$prompt
=
pht
(
'Land revision in the wrong state?'
);
id
(
new
PhutilConsoleBlock
())
->
addParagraph
(
tsprintf
(
'<bg:yellow>** %s **</bg>'
,
$state_header
))
->
addParagraph
(
tsprintf
(
'%B'
,
$state_warning
))
->
draw
();
$ok
=
phutil_console_confirm
(
$prompt
);
if
(!
$ok
)
{
throw
new
ArcanistUserAbortException
();
}
}
if
(
$rev_auxiliary
)
{
$phids
=
idx
(
$rev_auxiliary
,
'phabricator:depends-on'
,
array
());
if
(
$phids
)
{
$dep_on_revs
=
$this
->
getConduit
()->
callMethodSynchronous
(
'differential.query'
,
array
(
'phids'
=>
$phids
,
'status'
=>
'status-open'
,
));
$open_dep_revs
=
array
();
foreach
(
$dep_on_revs
as
$dep_on_rev
)
{
$dep_on_rev_id
=
$dep_on_rev
[
'id'
];
$dep_on_rev_title
=
$dep_on_rev
[
'title'
];
$dep_on_rev_status
=
$dep_on_rev
[
'status'
];
$open_dep_revs
[
$dep_on_rev_id
]
=
$dep_on_rev_title
;
}
if
(!
empty
(
$open_dep_revs
))
{
$open_revs
=
array
();
foreach
(
$open_dep_revs
as
$id
=>
$title
)
{
$open_revs
[]
=
' - D'
.
$id
.
': '
.
$title
;
}
$open_revs
=
implode
(
"
\n
"
,
$open_revs
);
echo
pht
(
"Revision '%s' depends on open revisions:
\n\n
%s"
,
"D{$rev_id}: {$rev_title}"
,
$open_revs
);
$ok
=
phutil_console_confirm
(
pht
(
'Continue anyway?'
));
if
(!
$ok
)
{
throw
new
ArcanistUserAbortException
();
}
}
}
}
$message
=
$this
->
getConduit
()->
callMethodSynchronous
(
'differential.getcommitmessage'
,
array
(
'revision_id'
=>
$rev_id
,
));
$this
->
messageFile
=
new
TempFile
();
Filesystem
::
writeFile
(
$this
->
messageFile
,
$message
);
echo
pht
(
"Landing revision '%s'..."
,
"D{$rev_id}: {$rev_title}"
).
"
\n
"
;
$diff_phid
=
idx
(
$this
->
revision
,
'activeDiffPHID'
);
if
(
$diff_phid
)
{
$this
->
checkForBuildables
(
$diff_phid
);
}
}
private
function
pullFromRemote
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
$local_ahead_of_remote
=
false
;
if
(
$this
->
isGit
)
{
$repository_api
->
execxLocal
(
'checkout %s'
,
$this
->
onto
);
echo
phutil_console_format
(
pht
(
"Switched to branch **%s**. Updating branch...
\n
"
,
$this
->
onto
));
try
{
$repository_api
->
execxLocal
(
'pull --ff-only --no-stat'
);
}
catch
(
CommandException
$ex
)
{
if
(!
$this
->
isGitSvn
)
{
throw
$ex
;
}
}
list
(
$out
)
=
$repository_api
->
execxLocal
(
'log %s..%s'
,
$this
->
ontoRemoteBranch
,
$this
->
onto
);
if
(
strlen
(
trim
(
$out
)))
{
$local_ahead_of_remote
=
true
;
}
else
if
(
$this
->
isGitSvn
)
{
$repository_api
->
execxLocal
(
'svn rebase'
);
}
}
else
if
(
$this
->
isHg
)
{
echo
phutil_console_format
(
pht
(
'Updating **%s**...'
,
$this
->
onto
).
"
\n
"
);
try
{
list
(
$out
,
$err
)
=
$repository_api
->
execxLocal
(
'pull'
);
$divergedbookmark
=
$this
->
onto
.
'@'
.
$repository_api
->
getBranchName
();
if
(
strpos
(
$err
,
$divergedbookmark
)
!==
false
)
{
throw
new
ArcanistUsageException
(
phutil_console_format
(
pht
(
"Local bookmark **%s** has diverged from the server's **%s** "
.
"(now labeled **%s**). Please resolve this divergence and run "
.
"'%s' again."
,
$this
->
onto
,
$this
->
onto
,
$divergedbookmark
,
'arc land'
)));
}
}
catch
(
CommandException
$ex
)
{
$err
=
$ex
->
getError
();
$stdout
=
$ex
->
getStdout
();
// Copied from: PhabricatorRepositoryPullLocalDaemon.php
// NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the
// behavior of "hg pull" to return 1 in case of a successful pull
// with no changes. This behavior has been reverted, but users who
// updated between Feb 1, 2012 and Mar 1, 2012 will have the
// erroring version. Do a dumb test against stdout to check for this
// possibility.
// See: https://github.com/phacility/phabricator/issues/101/
// NOTE: Mercurial has translated versions, which translate this error
// string. In a translated version, the string will be something else,
// like "aucun changement trouve". There didn't seem to be an easy way
// to handle this (there are hard ways but this is not a common
// problem and only creates log spam, not application failures).
// Assume English.
// TODO: Remove this once we're far enough in the future that
// deployment of 2.1 is exceedingly rare?
if
(
$err
!=
1
||
!
preg_match
(
'/no changes found/'
,
$stdout
))
{
throw
$ex
;
}
}
// Pull succeeded. Now make sure master is not on an outgoing change
if
(
$repository_api
->
supportsPhases
())
{
list
(
$out
)
=
$repository_api
->
execxLocal
(
'log -r %s --template %s'
,
$this
->
onto
,
'{phase}'
);
if
(
$out
!=
'public'
)
{
$local_ahead_of_remote
=
true
;
}
}
else
{
// execManual instead of execx because outgoing returns
// code 1 when there is nothing outgoing
list
(
$err
,
$out
)
=
$repository_api
->
execManualLocal
(
'outgoing -r %s'
,
$this
->
onto
);
// $err === 0 means something is outgoing
if
(
$err
===
0
)
{
$local_ahead_of_remote
=
true
;
}
}
}
if
(
$local_ahead_of_remote
)
{
throw
new
ArcanistUsageException
(
pht
(
"Local %s '%s' is ahead of remote %s '%s', so landing a feature "
.
"%s would push additional changes. Push or reset the changes in '%s' "
.
"before running '%s'."
,
$this
->
ontoType
,
$this
->
onto
,
$this
->
ontoType
,
$this
->
ontoRemoteBranch
,
$this
->
ontoType
,
$this
->
onto
,
'arc land'
));
}
}
private
function
rebase
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
chdir
(
$repository_api
->
getPath
());
if
(
$this
->
isHg
)
{
$onto_tip
=
$repository_api
->
getCanonicalRevisionName
(
$this
->
onto
);
$common_ancestor
=
$repository_api
->
getCanonicalRevisionName
(
hgsprintf
(
'ancestor(%s, %s)'
,
$this
->
onto
,
$this
->
branch
));
// Only rebase if the local branch is not at the tip of the onto branch.
if
(
$onto_tip
!=
$common_ancestor
)
{
// keep branch here so later we can decide whether to remove it
$err
=
$repository_api
->
execPassthru
(
'rebase -d %s --keepbranches'
,
$this
->
onto
);
if
(
$err
)
{
echo
phutil_console_format
(
"%s
\n
"
,
pht
(
'Aborting rebase'
));
$repository_api
->
execManualLocal
(
'rebase --abort'
);
$this
->
restoreBranch
();
throw
new
ArcanistUsageException
(
pht
(
"'%s' failed and the rebase was aborted. This is most "
.
"likely due to conflicts. Manually rebase %s onto %s, resolve "
.
"the conflicts, then run '%s' again."
,
sprintf
(
'hg rebase %s'
,
$this
->
onto
),
$this
->
branch
,
$this
->
onto
,
'arc land'
));
}
}
}
$repository_api
->
reloadWorkingCopy
();
}
private
function
squash
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$this
->
isGit
)
{
$repository_api
->
execxLocal
(
'checkout %s'
,
$this
->
onto
);
$repository_api
->
execxLocal
(
'merge --no-stat --squash --ff-only %s'
,
$this
->
branch
);
}
else
if
(
$this
->
isHg
)
{
// The hg code is a little more complex than git's because we
// need to handle the case where the landing branch has child branches:
// -a--------b master
// \
// w--x mybranch
// \--y subbranch1
// \--z subbranch2
//
// arc land --branch mybranch --onto master :
// -a--b--wx master
// \--y subbranch1
// \--z subbranch2
$branch_rev_id
=
$repository_api
->
getCanonicalRevisionName
(
$this
->
branch
);
// At this point $this->onto has been pulled from remote and
// $this->branch has been rebased on top of onto(by the rebase()
// function). So we're guaranteed to have onto as an ancestor of branch
// when we use first((onto::branch)-onto) below.
$branch_root
=
$repository_api
->
getCanonicalRevisionName
(
hgsprintf
(
'first((%s::%s)-%s)'
,
$this
->
onto
,
$this
->
branch
,
$this
->
onto
));
$branch_range
=
hgsprintf
(
'(%s::%s)'
,
$branch_root
,
$this
->
branch
);
if
(!
$this
->
keepBranch
)
{
$this
->
handleAlternateBranches
(
$branch_root
,
$branch_range
);
}
// Collapse just the landing branch onto master.
// Leave its children on the original branch.
$err
=
$repository_api
->
execPassthru
(
'rebase --collapse --keep --logfile %s -r %s -d %s'
,
$this
->
messageFile
,
$branch_range
,
$this
->
onto
);
if
(
$err
)
{
$repository_api
->
execManualLocal
(
'rebase --abort'
);
$this
->
restoreBranch
();
throw
new
ArcanistUsageException
(
pht
(
"Squashing the commits under %s failed. "
.
"Manually squash your commits and run '%s' again."
,
$this
->
branch
,
'arc land'
));
}
if
(
$repository_api
->
isBookmark
(
$this
->
branch
))
{
// a bug in mercurial means bookmarks end up on the revision prior
// to the collapse when using --collapse with --keep,
// so we manually move them to the correct spots
// see: http://bz.selenic.com/show_bug.cgi?id=3716
$repository_api
->
execxLocal
(
'bookmark -f %s'
,
$this
->
onto
);
$repository_api
->
execxLocal
(
'bookmark -f %s -r %s'
,
$this
->
branch
,
$branch_rev_id
);
}
// check if the branch had children
list
(
$output
)
=
$repository_api
->
execxLocal
(
'log -r %s --template %s'
,
hgsprintf
(
'children(%s)'
,
$this
->
branch
),
'{node}
\n
'
);
$child_branch_roots
=
phutil_split_lines
(
$output
,
false
);
$child_branch_roots
=
array_filter
(
$child_branch_roots
);
if
(
$child_branch_roots
)
{
// move the branch's children onto the collapsed commit
foreach
(
$child_branch_roots
as
$child_root
)
{
$repository_api
->
execxLocal
(
'rebase -d %s -s %s --keep --keepbranches'
,
$this
->
onto
,
$child_root
);
}
}
// All the rebases may have moved us to another branch
// so we move back.
$repository_api
->
execxLocal
(
'checkout %s'
,
$this
->
onto
);
}
}
/**
* Detect alternate branches and prompt the user for how to handle
* them. An alternate branch is a branch that forks from the landing
* branch prior to the landing branch tip.
*
* In a situation like this:
* -a--------b master
* \
* w--x landingbranch
* \ \-- g subbranch
* \--y altbranch1
* \--z altbranch2
*
* y and z are alternate branches and will get deleted by the squash,
* so we need to detect them and ask the user what they want to do.
*
* @param string The revision id of the landing branch's root commit.
* @param string The revset specifying all the commits in the landing branch.
* @return void
*/
private
function
handleAlternateBranches
(
$branch_root
,
$branch_range
)
{
$repository_api
=
$this
->
getRepositoryAPI
();
// Using the tree in the doccomment, the revset below resolves as follows:
// 1. roots(descendants(w) - descendants(x) - (w::x))
// 2. roots({x,g,y,z} - {g} - {w,x})
// 3. roots({y,z})
// 4. {y,z}
$alt_branch_revset
=
hgsprintf
(
'roots(descendants(%s)-descendants(%s)-%R)'
,
$branch_root
,
$this
->
branch
,
$branch_range
);
list
(
$alt_branches
)
=
$repository_api
->
execxLocal
(
'log --template %s -r %s'
,
'{node}
\n
'
,
$alt_branch_revset
);
$alt_branches
=
phutil_split_lines
(
$alt_branches
,
false
);
$alt_branches
=
array_filter
(
$alt_branches
);
$alt_count
=
count
(
$alt_branches
);
if
(
$alt_count
>
0
)
{
$input
=
phutil_console_prompt
(
pht
(
"%s '%s' has %s %s(s) forking off of it that would be deleted "
.
"during a squash. Would you like to keep a non-squashed copy, rebase "
.
"them on top of '%s', or abort and deal with them yourself? "
.
"(k)eep, (r)ebase, (a)bort:"
,
ucfirst
(
$this
->
branchType
),
$this
->
branch
,
$alt_count
,
$this
->
branchType
,
$this
->
branch
));
if
(
$input
==
'k'
||
$input
==
'keep'
)
{
$this
->
keepBranch
=
true
;
}
else
if
(
$input
==
'r'
||
$input
==
'rebase'
)
{
foreach
(
$alt_branches
as
$alt_branch
)
{
$repository_api
->
execxLocal
(
'rebase --keep --keepbranches -d %s -s %s'
,
$this
->
branch
,
$alt_branch
);
}
}
else
if
(
$input
==
'a'
||
$input
==
'abort'
)
{
$branch_string
=
implode
(
"
\n
"
,
$alt_branches
);
echo
"
\n
"
,
pht
(
"Remove the %s starting at these revisions and run %s again:
\n
%s"
,
$this
->
branchType
.
's'
,
$branch_string
,
'arc land'
),
"
\n\n
"
;
throw
new
ArcanistUserAbortException
();
}
else
{
throw
new
ArcanistUsageException
(
pht
(
'Invalid choice. Aborting arc land.'
));
}
}
}
private
function
merge
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
// In immutable histories, do a --no-ff merge to force a merge commit with
// the right message.
$repository_api
->
execxLocal
(
'checkout %s'
,
$this
->
onto
);
chdir
(
$repository_api
->
getPath
());
if
(
$this
->
isGit
)
{
$err
=
phutil_passthru
(
'git merge --no-stat --no-ff --no-commit %s'
,
$this
->
branch
);
if
(
$err
)
{
throw
new
ArcanistUsageException
(
pht
(
"'%s' failed. Your working copy has been left in a partially "
.
"merged state. You can: abort with '%s'; or follow the "
.
"instructions to complete the merge."
,
'git merge'
,
'git merge --abort'
));
}
}
else
if
(
$this
->
isHg
)
{
// HG arc land currently doesn't support --merge.
// When merging a bookmark branch to a master branch that
// hasn't changed since the fork, mercurial fails to merge.
// Instead of only working in some cases, we just disable --merge
// until there is a demand for it.
// The user should never reach this line, since --merge is
// forbidden at the command line argument level.
throw
new
ArcanistUsageException
(
pht
(
'%s is not currently supported for hg repos.'
,
'--merge'
));
}
}
private
function
push
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
// These commands can fail legitimately (e.g. commit hooks)
try
{
if
(
$this
->
isGit
)
{
$repository_api
->
execxLocal
(
'commit -F %s'
,
$this
->
messageFile
);
if
(
phutil_is_windows
())
{
// Occasionally on large repositories on Windows, Git can exit with
// an unclean working copy here. This prevents reverts from being
// pushed to the remote when this occurs.
$this
->
requireCleanWorkingCopy
();
}
}
else
if
(
$this
->
isHg
)
{
// hg rebase produces a commit earlier as part of rebase
if
(!
$this
->
useSquash
)
{
$repository_api
->
execxLocal
(
'commit --logfile %s'
,
$this
->
messageFile
);
}
}
// We dispatch this event so we can run checks on the merged revision,
// right before it gets pushed out. It's easier to do this in arc land
// than to try to hook into git/hg.
$this
->
didCommitMerge
();
}
catch
(
Exception
$ex
)
{
$this
->
executeCleanupAfterFailedPush
();
throw
$ex
;
}
if
(
$this
->
getArgument
(
'hold'
))
{
echo
phutil_console_format
(
pht
(
'Holding change in **%s**: it has NOT been pushed yet.'
,
$this
->
onto
).
"
\n
"
);
}
else
{
echo
pht
(
'Pushing change...'
),
"
\n\n
"
;
chdir
(
$repository_api
->
getPath
());
if
(
$this
->
isGitSvn
)
{
$err
=
phutil_passthru
(
'git svn dcommit'
);
$cmd
=
'git svn dcommit'
;
}
else
if
(
$this
->
isGit
)
{
$err
=
phutil_passthru
(
'git push %s %s'
,
$this
->
remote
,
$this
->
onto
);
$cmd
=
'git push'
;
}
else
if
(
$this
->
isHgSvn
)
{
// hg-svn doesn't support 'push -r', so we do a normal push
// which hg-svn modifies to only push the current branch and
// ancestors.
$err
=
$repository_api
->
execPassthru
(
'push %s'
,
$this
->
remote
);
$cmd
=
'hg push'
;
}
else
if
(
$this
->
isHg
)
{
if
(
strlen
(
$this
->
remote
))
{
$err
=
$repository_api
->
execPassthru
(
'push -r %s %s'
,
$this
->
onto
,
$this
->
remote
);
}
else
{
$err
=
$repository_api
->
execPassthru
(
'push -r %s'
,
$this
->
onto
);
}
$cmd
=
'hg push'
;
}
if
(
$err
)
{
echo
phutil_console_format
(
"<bg:red>** %s **</bg>
\n
"
,
pht
(
'PUSH FAILED!'
));
$this
->
executeCleanupAfterFailedPush
();
if
(
$this
->
isGit
)
{
throw
new
ArcanistUsageException
(
pht
(
"'%s' failed! Fix the error and run '%s' again."
,
$cmd
,
'arc land'
));
}
throw
new
ArcanistUsageException
(
pht
(
"'%s' failed! Fix the error and push this change manually."
,
$cmd
));
}
$this
->
didPush
();
echo
"
\n
"
;
}
}
private
function
executeCleanupAfterFailedPush
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$this
->
isGit
)
{
$repository_api
->
execxLocal
(
'reset --hard HEAD^'
);
$this
->
restoreBranch
();
}
else
if
(
$this
->
isHg
)
{
$repository_api
->
execxLocal
(
'--config extensions.mq= strip %s'
,
$this
->
onto
);
$this
->
restoreBranch
();
}
}
private
function
cleanupBranch
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
echo
pht
(
'Cleaning up feature %s...'
,
$this
->
branchType
),
"
\n
"
;
if
(
$this
->
isGit
)
{
list
(
$ref
)
=
$repository_api
->
execxLocal
(
'rev-parse --verify %s'
,
$this
->
branch
);
$ref
=
trim
(
$ref
);
$recovery_command
=
csprintf
(
'git checkout -b %s %s'
,
$this
->
branch
,
$ref
);
echo
pht
(
'(Use `%s` if you want it back.)'
,
$recovery_command
),
"
\n
"
;
$repository_api
->
execxLocal
(
'branch -D %s'
,
$this
->
branch
);
}
else
if
(
$this
->
isHg
)
{
$common_ancestor
=
$repository_api
->
getCanonicalRevisionName
(
hgsprintf
(
'ancestor(%s,%s)'
,
$this
->
onto
,
$this
->
branch
));
$branch_root
=
$repository_api
->
getCanonicalRevisionName
(
hgsprintf
(
'first((%s::%s)-%s)'
,
$common_ancestor
,
$this
->
branch
,
$common_ancestor
));
$repository_api
->
execxLocal
(
'--config extensions.mq= strip -r %s'
,
$branch_root
);
if
(
$repository_api
->
isBookmark
(
$this
->
branch
))
{
$repository_api
->
execxLocal
(
'bookmark -d %s'
,
$this
->
branch
);
}
}
if
(
$this
->
getArgument
(
'delete-remote'
))
{
if
(
$this
->
isHg
)
{
// named branches were closed as part of the earlier commit
// so only worry about bookmarks
if
(
$repository_api
->
isBookmark
(
$this
->
branch
))
{
$repository_api
->
execxLocal
(
'push -B %s %s'
,
$this
->
branch
,
$this
->
remote
);
}
}
}
}
public
function
getSupportedRevisionControlSystems
()
{
return
array
(
'git'
,
'hg'
);
}
private
function
getBranchOrBookmark
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$this
->
isGit
)
{
$branch
=
$repository_api
->
getBranchName
();
// If we don't have a branch name, just use whatever's at HEAD.
if
(!
strlen
(
$branch
)
&&
!
$this
->
isGitSvn
)
{
$branch
=
$repository_api
->
getWorkingCopyRevision
();
}
}
else
if
(
$this
->
isHg
)
{
$branch
=
$repository_api
->
getActiveBookmark
();
if
(!
$branch
)
{
$branch
=
$repository_api
->
getBranchName
();
}
}
return
$branch
;
}
private
function
getBranchType
(
$branch
)
{
$repository_api
=
$this
->
getRepositoryAPI
();
if
(
$this
->
isHg
&&
$repository_api
->
isBookmark
(
$branch
))
{
return
'bookmark'
;
}
return
'branch'
;
}
/**
* Restore the original branch, e.g. after a successful land or a failed
* pull.
*/
private
function
restoreBranch
()
{
$repository_api
=
$this
->
getRepositoryAPI
();
$repository_api
->
execxLocal
(
'checkout %s'
,
$this
->
oldBranch
);
if
(
$this
->
isGit
)
{
$repository_api
->
execxLocal
(
'submodule update --init --recursive'
);
}
echo
pht
(
"Switched back to %s %s.
\n
"
,
$this
->
branchType
,
phutil_console_format
(
'**%s**'
,
$this
->
oldBranch
));
}
/**
* Check if a diff has a running or failed buildable, and prompt the user
* before landing if it does.
*/
private
function
checkForBuildables
(
$diff_phid
)
{
// Try to use the more modern check which respects the "Warn on Land"
// behavioral flag on build plans if we can. This newer check won't work
// unless the server is running code from March 2019 or newer since the
// API methods we need won't exist yet. We'll fall back to the older check
// if this one doesn't work out.
try
{
$this
->
checkForBuildablesWithPlanBehaviors
(
$diff_phid
);
return
;
}
catch
(
ArcanistUserAbortException
$abort_ex
)
{
throw
$abort_ex
;
}
catch
(
Exception
$ex
)
{
// Continue with the older approach, below.
}
// NOTE: Since Harbormaster is still beta and this stuff all got added
// recently, just bail if we can't find a buildable. This is just an
// advisory check intended to prevent human error.
try
{
$buildables
=
$this
->
getConduit
()->
callMethodSynchronous
(
'harbormaster.querybuildables'
,
array
(
'buildablePHIDs'
=>
array
(
$diff_phid
),
'manualBuildables'
=>
false
,
));
}
catch
(
ConduitClientException
$ex
)
{
return
;
}
if
(!
$buildables
[
'data'
])
{
// If there's no corresponding buildable, we're done.
return
;
}
$console
=
PhutilConsole
::
getConsole
();
$buildable
=
head
(
$buildables
[
'data'
]);
if
(
$buildable
[
'buildableStatus'
]
==
'passed'
)
{
$console
->
writeOut
(
"**<bg:green> %s </bg>** %s
\n
"
,
pht
(
'BUILDS PASSED'
),
pht
(
'Harbormaster builds for the active diff completed successfully.'
));
return
;
}
switch
(
$buildable
[
'buildableStatus'
])
{
case
'building'
:
$message
=
pht
(
'Harbormaster is still building the active diff for this revision.'
);
$prompt
=
pht
(
'Land revision anyway, despite ongoing build?'
);
break
;
case
'failed'
:
$message
=
pht
(
'Harbormaster failed to build the active diff for this revision.'
);
$prompt
=
pht
(
'Land revision anyway, despite build failures?'
);
break
;
default
:
// If we don't recognize the status, just bail.
return
;
}
$builds
=
$this
->
queryBuilds
(
array
(
'buildablePHIDs'
=>
array
(
$buildable
[
'phid'
]),
));
$console
->
writeOut
(
$message
.
"
\n\n
"
);
$builds
=
msortv
(
$builds
,
'getStatusSortVector'
);
foreach
(
$builds
as
$build
)
{
$ansi_color
=
$build
->
getStatusANSIColor
();
$status_name
=
$build
->
getStatusName
();
$object_name
=
$build
->
getObjectName
();
$build_name
=
$build
->
getName
();
echo
tsprintf
(
" **<bg:"
.
$ansi_color
.
"> %s </bg>** %s: %s
\n
"
,
$status_name
,
$object_name
,
$build_name
);
}
$console
->
writeOut
(
"
\n
%s
\n\n
**%s**: __%s__"
,
pht
(
'You can review build details here:'
),
pht
(
'Harbormaster URI'
),
$buildable
[
'uri'
]);
if
(!
phutil_console_confirm
(
$prompt
))
{
throw
new
ArcanistUserAbortException
();
}
}
private
function
checkForBuildablesWithPlanBehaviors
(
$diff_phid
)
{
// TODO: These queries should page through all results instead of fetching
// only the first page, but we don't have good primitives to support that
// in "master" yet.
$this
->
writeInfo
(
pht
(
'BUILDS'
),
pht
(
'Checking build status...'
));
$raw_buildables
=
$this
->
getConduit
()->
callMethodSynchronous
(
'harbormaster.buildable.search'
,
array
(
'constraints'
=>
array
(
'objectPHIDs'
=>
array
(
$diff_phid
,
),
'manual'
=>
false
,
),
));
if
(!
$raw_buildables
[
'data'
])
{
return
;
}
$buildables
=
$raw_buildables
[
'data'
];
$buildable_phids
=
ipull
(
$buildables
,
'phid'
);
$raw_builds
=
$this
->
getConduit
()->
callMethodSynchronous
(
'harbormaster.build.search'
,
array
(
'constraints'
=>
array
(
'buildables'
=>
$buildable_phids
,
),
));
if
(!
$raw_builds
[
'data'
])
{
return
;
}
$builds
=
array
();
foreach
(
$raw_builds
[
'data'
]
as
$raw_build
)
{
$build_ref
=
ArcanistBuildRef
::
newFromConduit
(
$raw_build
);
$build_phid
=
$build_ref
->
getPHID
();
$builds
[
$build_phid
]
=
$build_ref
;
}
$plan_phids
=
mpull
(
$builds
,
'getBuildPlanPHID'
);
$plan_phids
=
array_values
(
$plan_phids
);
$raw_plans
=
$this
->
getConduit
()->
callMethodSynchronous
(
'harbormaster.buildplan.search'
,
array
(
'constraints'
=>
array
(
'phids'
=>
$plan_phids
,
),
));
$plans
=
array
();
foreach
(
$raw_plans
[
'data'
]
as
$raw_plan
)
{
$plan_ref
=
ArcanistBuildPlanRef
::
newFromConduit
(
$raw_plan
);
$plan_phid
=
$plan_ref
->
getPHID
();
$plans
[
$plan_phid
]
=
$plan_ref
;
}
$ongoing_builds
=
array
();
$failed_builds
=
array
();
$builds
=
msortv
(
$builds
,
'getStatusSortVector'
);
foreach
(
$builds
as
$build_ref
)
{
$plan
=
idx
(
$plans
,
$build_ref
->
getBuildPlanPHID
());
if
(!
$plan
)
{
continue
;
}
$plan_behavior
=
$plan
->
getBehavior
(
'arc-land'
,
'always'
);
$if_building
=
(
$plan_behavior
==
'building'
);
$if_complete
=
(
$plan_behavior
==
'complete'
);
$if_never
=
(
$plan_behavior
==
'never'
);
// If the build plan "Never" warns when landing, skip it.
if
(
$if_never
)
{
continue
;
}
// If the build plan warns when landing "If Complete" but the build is
// not complete, skip it.
if
(
$if_complete
&&
!
$build_ref
->
isComplete
())
{
continue
;
}
// If the build plan warns when landing "If Building" but the build is
// complete, skip it.
if
(
$if_building
&&
$build_ref
->
isComplete
())
{
continue
;
}
// Ignore passing builds.
if
(
$build_ref
->
isPassed
())
{
continue
;
}
if
(!
$build_ref
->
isComplete
())
{
$ongoing_builds
[]
=
$build_ref
;
}
else
{
$failed_builds
[]
=
$build_ref
;
}
}
if
(!
$ongoing_builds
&&
!
$failed_builds
)
{
return
;
}
if
(
$failed_builds
)
{
$this
->
writeWarn
(
pht
(
'BUILD FAILURES'
),
pht
(
'Harbormaster failed to build the active diff for this revision:'
));
$prompt
=
pht
(
'Land revision anyway, despite build failures?'
);
}
else
if
(
$ongoing_builds
)
{
$this
->
writeWarn
(
pht
(
'ONGOING BUILDS'
),
pht
(
'Harbormaster is still building the active diff for this revision:'
));
$prompt
=
pht
(
'Land revision anyway, despite ongoing build?'
);
}
$show_builds
=
array_merge
(
$failed_builds
,
$ongoing_builds
);
echo
"
\n
"
;
foreach
(
$show_builds
as
$build_ref
)
{
$ansi_color
=
$build_ref
->
getStatusANSIColor
();
$status_name
=
$build_ref
->
getStatusName
();
$object_name
=
$build_ref
->
getObjectName
();
$build_name
=
$build_ref
->
getName
();
echo
tsprintf
(
" **<bg:"
.
$ansi_color
.
"> %s </bg>** %s: %s
\n
"
,
$status_name
,
$object_name
,
$build_name
);
}
echo
tsprintf
(
"
\n
%s
\n\n
"
,
pht
(
'You can review build details here:'
));
foreach
(
$buildables
as
$buildable
)
{
$buildable_uri
=
id
(
new
PhutilURI
(
$this
->
getConduitURI
()))
->
setPath
(
sprintf
(
'/B%d'
,
$buildable
[
'id'
]));
echo
tsprintf
(
" **%s**: __%s__
\n
"
,
pht
(
'Buildable %d'
,
$buildable
[
'id'
]),
$buildable_uri
);
}
if
(!
phutil_console_confirm
(
$prompt
))
{
throw
new
ArcanistUserAbortException
();
}
}
public
function
buildEngineMessage
(
ArcanistLandEngine
$engine
)
{
// TODO: This is oh-so-gross.
$this
->
findRevision
();
$engine
->
setCommitMessageFile
(
$this
->
messageFile
);
}
public
function
didCommitMerge
()
{
$this
->
dispatchEvent
(
ArcanistEventType
::
TYPE_LAND_WILLPUSHREVISION
,
array
());
}
public
function
didPush
()
{
$this
->
askForRepositoryUpdate
();
$mark_workflow
=
$this
->
buildChildWorkflow
(
'close-revision'
,
array
(
'--finalize'
,
'--quiet'
,
$this
->
revision
[
'id'
],
));
$mark_workflow
->
run
();
}
private
function
queryBuilds
(
array
$constraints
)
{
$conduit
=
$this
->
getConduit
();
// NOTE: This method only loads the 100 most recent builds. It's rare for
// a revision to have more builds than that and there's currently no paging
// wrapper for "*.search" Conduit API calls available in Arcanist.
try
{
$raw_result
=
$conduit
->
callMethodSynchronous
(
'harbormaster.build.search'
,
array
(
'constraints'
=>
$constraints
,
));
}
catch
(
Exception
$ex
)
{
// If the server doesn't have "harbormaster.build.search" yet (Aug 2016),
// try the older "harbormaster.querybuilds" instead.
$raw_result
=
$conduit
->
callMethodSynchronous
(
'harbormaster.querybuilds'
,
$constraints
);
}
$refs
=
array
();
foreach
(
$raw_result
[
'data'
]
as
$raw_data
)
{
$refs
[]
=
ArcanistBuildRef
::
newFromConduit
(
$raw_data
);
}
return
$refs
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Nov 22, 10:46 (5 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
547687
Default Alt Text
ArcanistLandWorkflow.php (49 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment