Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F995718
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/workflow/land/ArcanistLandWorkflow.php b/src/workflow/land/ArcanistLandWorkflow.php
index 76f10da6..4dbfabb7 100644
--- a/src/workflow/land/ArcanistLandWorkflow.php
+++ b/src/workflow/land/ArcanistLandWorkflow.php
@@ -1,310 +1,317 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Lands a branch by rebasing, merging and amending it.
*
* @group workflow
*/
final class ArcanistLandWorkflow extends ArcanistBaseWorkflow {
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
- **land** __branch__ [--onto __master__]
+ **land** [__options__] __branch__ [--onto __master__]
Supports: git
Land an accepted change (currently sitting in local feature branch
__branch__) onto __master__ and push it to the remote. Then, delete
the feature branch.
In mutable repositories, this will perform a --squash merge (the
entire branch will be represented by one commit on __master__). In
- immutable repositories, it will perform a --no-ff merge (the branch
- will always be merged into __master__ with a merge commit).
+ immutable repositories (or when --merge is provided), it will perform
+ a --no-ff merge (the branch will always be merged into __master__ with
+ a merge commit).
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' => "Land feature branch onto a branch other than ".
"'master' (default).",
),
'hold' => array(
'help' => "Prepare the change to be pushed, but do not actually ".
"push it.",
),
'keep-branch' => array(
'help' => "Keep the feature branch after pushing changes to the ".
"remote (by default, it is deleted).",
),
'remote' => array(
'param' => 'origin',
'help' => "Push to a remote other than 'origin' (default).",
),
+ 'merge' => array(
+ 'help' => 'Perform a --no-ff merge, not a --squash merge. If the '.
+ 'project is marked as having an immutable history, this is '.
+ 'the default behavior.',
+ ),
'*' => 'branch',
);
}
public function run() {
$branch = $this->getArgument('branch');
if (count($branch) !== 1) {
throw new ArcanistUsageException(
"Specify exactly one branch to land changes from.");
}
$branch = head($branch);
$remote = $this->getArgument('remote', 'origin');
$onto = $this->getArgument('onto', 'master');
- $is_immutable = $this->isHistoryImmutable();
+ $is_immutable = $this->isHistoryImmutable() ||
+ $this->getArgument('merge');
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistGitAPI)) {
throw new ArcanistUsageException("'arc land' only supports git.");
}
list($err) = exec_manual(
'(cd %s && git rev-parse --verify %s)',
$repository_api->getPath(),
$branch);
if ($err) {
throw new ArcanistUsageException("Branch '{$branch}' does not exist.");
}
$this->requireCleanWorkingCopy();
$repository_api->parseRelativeLocalCommit(array($remote.'/'.$onto));
$old_branch = $repository_api->getBranchName();
execx(
'(cd %s && git checkout %s)',
$repository_api->getPath(),
$onto);
echo phutil_console_format(
"Switched to branch **%s**. Updating branch...\n",
$onto);
execx(
'(cd %s && git pull --ff-only)',
$repository_api->getPath());
list($out) = execx(
'(cd %s && git log %s/%s..%s)',
$repository_api->getPath(),
$remote,
$onto,
$onto);
if (strlen(trim($out))) {
throw new ArcanistUsageException(
"Local branch '{$onto}' is ahead of '{$remote}/{$onto}', so landing ".
"a feature branch would push additional changes. Push or reset the ".
"changes in '{$onto}' before running 'arc land'.");
}
execx(
'(cd %s && git checkout %s)',
$repository_api->getPath(),
$branch);
echo phutil_console_format(
"Switched to branch **%s**. Identifying and merging...\n",
$branch);
if (!$is_immutable) {
$err = phutil_passthru(
'(cd %s && git rebase %s)',
$repository_api->getPath(),
$onto);
if ($err) {
throw new ArcanistUsageException(
"'git rebase {$onto}' failed. You can abort with 'git rebase ".
"--abort', or resolve conflicts and use 'git rebase --continue' to ".
"continue forward. After resolving the rebase, run 'arc land' ".
"again.");
}
// Now that we've rebased, the merge-base of origin/master and HEAD may
// be different. Reparse the relative commit.
$repository_api->parseRelativeLocalCommit(array($remote.'/'.$onto));
}
$revisions = $repository_api->loadWorkingCopyDifferentialRevisions(
$this->getConduit(),
array(
'authors' => array($this->getUserPHID()),
));
if (!count($revisions)) {
throw new ArcanistUsageException(
"arc can not identify which revision exists on branch '{$branch}'. ".
"Update the revision with recent changes to synchronize the branch ".
"name and hashes, or use 'arc amend' to amend the commit message at ".
"HEAD.");
} else if (count($revisions) > 1) {
$message =
"There are multiple revisions on feature branch '{$branch}' which are ".
"not present on '{$onto}':\n\n".
$this->renderRevisionList($revisions)."\n".
"Separate these revisions onto different branches, or manually land ".
"them in '{$onto}'.";
throw new ArcanistUsageException($message);
}
$revision = head($revisions);
$rev_id = $revision['id'];
$rev_title = $revision['title'];
if ($revision['status'] != ArcanistDifferentialRevisionStatus::ACCEPTED) {
$ok = phutil_console_confirm(
- "Revision 'D{$id}: {$rev_title}' has not been accepted. Continue ".
+ "Revision 'D{$rev_id}: {$rev_title}' has not been accepted. Continue ".
"anyway?");
if (!$ok) {
throw new ArcanistUserAbortException();
}
}
echo "Landing revision 'D{$rev_id}: {$rev_title}'...\n";
$message = $this->getConduit()->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision['id'],
));
execx(
'(cd %s && git checkout %s)',
$repository_api->getPath(),
$onto);
if ($is_immutable) {
// In immutable histories, do a --no-ff merge to force a merge commit with
// the right message.
$err = phutil_passthru(
'(cd %s && git merge --no-ff -m %s %s)',
$repository_api->getPath(),
$message,
$branch);
if ($err) {
throw new ArcanistUsageException(
"'git merge' failed. Your working copy has been left in a partially ".
"merged state. You can: abort with 'git merge --abort'; or follow ".
- "the instructions to complete the merge, and then push.");
+ "the instructions to complete the merge.");
}
} else {
// In mutable histories, do a --squash merge.
execx(
'(cd %s && git merge --squash --ff-only %s)',
$repository_api->getPath(),
$branch);
execx(
'(cd %s && git commit -m %s)',
$repository_api->getPath(),
$message);
}
if ($this->getArgument('hold')) {
echo phutil_console_format(
"Holding change in **%s**: it has NOT been pushed yet.\n",
$onto);
} else {
echo "Pushing change...\n\n";
$err = phutil_passthru(
'(cd %s && git push %s %s)',
$repository_api->getPath(),
$remote,
$onto);
if ($err) {
throw new ArcanistUsageException("'git push' failed.");
}
$mark_workflow = $this->buildChildWorkflow(
'mark-committed',
array(
'--finalize',
'--quiet',
$revision['id'],
));
$mark_workflow->run();
echo "\n";
}
if (!$this->getArgument('keep-branch')) {
list($ref) = execx(
'(cd %s && git rev-parse --verify %s)',
$repository_api->getPath(),
$branch);
$ref = trim($ref);
$recovery_command = csprintf(
'git checkout -b %s %s',
$branch,
$ref);
echo "Cleaning up feature branch...\n";
echo "(Use `{$recovery_command}` if you want it back.)\n";
execx(
'(cd %s && git branch -D %s)',
$repository_api->getPath(),
$branch);
}
// If we were on some branch A and the user ran "arc land B", switch back
// to A.
if (($old_branch != $branch) && ($old_branch != $onto)) {
execx(
'(cd %s && git checkout %s)',
$repository_api->getPath(),
$old_branch);
echo phutil_console_format(
"Switched back to branch **%s**.\n",
$old_branch);
}
echo "Done.\n";
return 0;
}
protected function getSupportedRevisionControlSystems() {
return array('git');
}
}
diff --git a/src/workflow/merge/ArcanistMergeWorkflow.php b/src/workflow/merge/ArcanistMergeWorkflow.php
index 0271c127..67ba55e6 100644
--- a/src/workflow/merge/ArcanistMergeWorkflow.php
+++ b/src/workflow/merge/ArcanistMergeWorkflow.php
@@ -1,163 +1,167 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Merges a branch using "git merge" or "hg merge", using a template commit
* message from Differential.
*
* @group workflow
*/
final class ArcanistMergeWorkflow extends ArcanistBaseWorkflow {
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
**merge** [__branch__] [--revision __revision_id__] [--show]
- Supports: git, hg
- Execute a "git merge <branch>" or "hg merge --rev <branch>" of a
- reviewed branch, but give the merge commit a useful commit message
- with information from Differential.
+ Supports: hg
+ Execute a "hg merge --rev <branch>" of a reviewed branch, but give the
+ merge commit a useful commit message with information from
+ Differential.
- In Git, this operates like "git merge <branch>" and should be executed
- from the branch you want to merge __into__, just like "git merge".
- Branch is required.
-
- In Mercurial, this operates like "hg merge" (default) or
- "hg merge --rev <branch>" and should be executed from the branch you
- want to merge __from__, just like "hg merge". It will also effect an
- "hg commit" with a rich commit message.
+ Tthis operates like "hg merge" (default) or "hg merge --rev <branch>"
+ and should be executed from the branch you want to merge __from__,
+ just like "hg merge". It will also effect an "hg commit" with a rich
+ commit message if possible.
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(
'show' => array(
'help' =>
"Don't merge, just show the commit message."
),
'revision' => array(
'param' => 'revision',
'help' =>
"Use the message for a specific revision. If 'arc' can't figure ".
"out which revision you want, you can tell it explicitly.",
),
'*' => 'branch',
);
}
public function run() {
+ $repository_api = $this->getRepositoryAPI();
+
+ if ($repository_api instanceof ArcanistGitAPI) {
+ throw new ArcanistUsageException(
+ "'arc merge' no longer supports git. Use ".
+ "'arc land --keep-branch --hold --merge <feature_branch>' instead.");
+ }
+
$this->writeStatusMessage(
phutil_console_format(
"**WARNING:** 'arc merge' is new and experimental.\n"));
- $repository_api = $this->getRepositoryAPI();
+
if (!$repository_api->supportsLocalBranchMerge()) {
$name = $repository_api->getSourceControlSystemName();
throw new ArcanistUsageException(
"This source control system ('{$name}') does not support 'arc merge'.");
}
if ($repository_api->getUncommittedChanges()) {
throw new ArcanistUsageException(
"You have uncommitted changes in this working copy. Commit ".
"(or revert) them before proceeding.");
}
$branch = $this->getArgument('branch');
if (count($branch) > 1) {
throw new ArcanistUsageException("Specify only one branch to merge.");
} else {
$branch = head($branch);
}
$conduit = $this->getConduit();
$revisions = $conduit->callMethodSynchronous(
'differential.find',
array(
'guids' => array($this->getUserPHID()),
'query' => 'committable',
));
// TODO: Make an effort to guess which revision the user means here. Branch
// name is a very strong heuristic but Conduit doesn't make it easy to get
// right now. We now also have "commits:local" after D857. Between these
// we should be able to get this right automatically in essentially every
// reasonable case.
try {
$revision = $this->chooseRevision(
$revisions,
$this->getArgument('revision'),
'Which revision do you want to merge?');
$revision_id = $revision->getID();
} catch (ArcanistChooseInvalidRevisionException $ex) {
throw new ArcanistUsageException(
"You can only merge Differential revisions which have been accepted.");
} catch (ArcanistChooseNoRevisionsException $ex) {
throw new ArcanistUsageException(
"You have no accepted Differential revisions.");
}
$message = $conduit->callMethodSynchronous(
'differential.getcommitmessage',
array(
'revision_id' => $revision_id,
'edit' => false,
));
if ($this->getArgument('show')) {
echo $message."\n";
} else {
$repository_api->performLocalBranchMerge($branch, $message);
echo "Merged '{$branch}'.\n";
$mark_workflow = $this->buildChildWorkflow(
'mark-committed',
array(
'--finalize',
$revision_id,
));
$mark_workflow->run();
}
return 0;
}
protected function getSupportedRevisionControlSystems() {
- return array('git', 'hg');
+ return array('hg');
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 13:57 (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
556934
Default Alt Text
(16 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment