2011-01-09 15:22:25 -08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
2011-12-02 16:21:14 -08:00
|
|
|
* Copyright 2012 Facebook, Inc.
|
2011-01-09 15:22:25 -08:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2011-02-19 11:36:08 -08:00
|
|
|
/**
|
|
|
|
* Interfaces with Git working copies.
|
|
|
|
*
|
|
|
|
* @group workingcopy
|
|
|
|
*/
|
2011-01-09 15:22:25 -08:00
|
|
|
class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|
|
|
|
|
|
|
private $status;
|
|
|
|
private $relativeCommit = null;
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
private $repositoryHasNoCommits = false;
|
2011-01-09 15:22:25 -08:00
|
|
|
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For the repository's initial commit, 'git diff HEAD^' and similar do
|
|
|
|
* not work. Using this instead does work.
|
|
|
|
*/
|
|
|
|
const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
|
|
|
|
|
|
public static function newHookAPI($root) {
|
|
|
|
return new ArcanistGitAPI($root);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSourceControlSystemName() {
|
|
|
|
return 'git';
|
|
|
|
}
|
|
|
|
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
public function getHasCommits() {
|
|
|
|
return !$this->repositoryHasNoCommits;
|
|
|
|
}
|
|
|
|
|
2011-01-09 15:22:25 -08:00
|
|
|
public function setRelativeCommit($relative_commit) {
|
|
|
|
$this->relativeCommit = $relative_commit;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2011-08-23 18:48:55 -07:00
|
|
|
public function getLocalCommitInformation() {
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
if ($this->repositoryHasNoCommits) {
|
|
|
|
// Zero commits.
|
|
|
|
throw new Exception(
|
|
|
|
"You can't get local commit information for a repository with no ".
|
|
|
|
"commits.");
|
|
|
|
} else if ($this->relativeCommit == self::GIT_MAGIC_ROOT_COMMIT) {
|
|
|
|
// One commit.
|
|
|
|
$against = 'HEAD';
|
|
|
|
} else {
|
|
|
|
// 2..N commits.
|
|
|
|
$against = $this->getRelativeCommit().'..HEAD';
|
|
|
|
}
|
|
|
|
|
2011-08-23 18:48:55 -07:00
|
|
|
list($info) = execx(
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
'(cd %s && git log %s --format=%s --)',
|
2011-08-23 18:48:55 -07:00
|
|
|
$this->getPath(),
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
$against,
|
2011-08-23 18:48:55 -07:00
|
|
|
'%H%x00%T%x00%P%x00%at%x00%an%x00%s');
|
|
|
|
|
|
|
|
$commits = array();
|
|
|
|
|
|
|
|
$info = trim($info);
|
|
|
|
$info = explode("\n", $info);
|
|
|
|
foreach ($info as $line) {
|
|
|
|
list($commit, $tree, $parents, $time, $author, $title)
|
|
|
|
= explode("\0", $line, 6);
|
|
|
|
|
|
|
|
$commits[] = array(
|
|
|
|
'commit' => $commit,
|
|
|
|
'tree' => $tree,
|
|
|
|
'parents' => array_filter(explode(' ', $parents)),
|
|
|
|
'time' => $time,
|
|
|
|
'author' => $author,
|
|
|
|
'summary' => $title,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $commits;
|
|
|
|
}
|
|
|
|
|
2011-01-09 15:22:25 -08:00
|
|
|
public function getRelativeCommit() {
|
|
|
|
if ($this->relativeCommit === null) {
|
|
|
|
list($err) = exec_manual(
|
|
|
|
'(cd %s; git rev-parse --verify HEAD^)',
|
|
|
|
$this->getPath());
|
|
|
|
if ($err) {
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
list($err) = exec_manual(
|
|
|
|
'(cd %s; git rev-parse --verify HEAD)',
|
|
|
|
$this->getPath());
|
|
|
|
if ($err) {
|
|
|
|
$this->repositoryHasNoCommits = true;
|
|
|
|
}
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT;
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
|
2011-01-09 15:22:25 -08:00
|
|
|
} else {
|
|
|
|
$this->relativeCommit = 'HEAD^';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $this->relativeCommit;
|
|
|
|
}
|
|
|
|
|
2011-05-29 10:45:18 -07:00
|
|
|
private function getDiffFullOptions() {
|
2011-01-09 15:22:25 -08:00
|
|
|
$options = array(
|
2011-05-29 10:45:18 -07:00
|
|
|
self::getDiffBaseOptions(),
|
2011-01-09 15:22:25 -08:00
|
|
|
'-M',
|
|
|
|
'-C',
|
|
|
|
'--no-color',
|
|
|
|
'--src-prefix=a/',
|
|
|
|
'--dst-prefix=b/',
|
|
|
|
'-U'.$this->getDiffLinesOfContext(),
|
|
|
|
);
|
|
|
|
return implode(' ', $options);
|
|
|
|
}
|
|
|
|
|
2011-05-29 10:45:18 -07:00
|
|
|
private function getDiffBaseOptions() {
|
|
|
|
$options = array(
|
|
|
|
// Disable external diff drivers, like graphical differs, since Arcanist
|
|
|
|
// needs to capture the diff text.
|
|
|
|
'--no-ext-diff',
|
|
|
|
// Disable textconv so we treat binary files as binary, even if they have
|
|
|
|
// an alternative textual representation. TODO: Ideally, Differential
|
|
|
|
// would ship up the binaries for 'arc patch' but display the textconv
|
|
|
|
// output in the visual diff.
|
|
|
|
'--no-textconv',
|
|
|
|
);
|
|
|
|
return implode(' ', $options);
|
|
|
|
}
|
|
|
|
|
2011-01-09 15:22:25 -08:00
|
|
|
public function getFullGitDiff() {
|
2011-05-29 10:45:18 -07:00
|
|
|
$options = $this->getDiffFullOptions();
|
2011-01-09 15:22:25 -08:00
|
|
|
list($stdout) = execx(
|
|
|
|
"(cd %s; git diff {$options} %s --)",
|
|
|
|
$this->getPath(),
|
|
|
|
$this->getRelativeCommit());
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getRawDiffText($path) {
|
2011-05-29 10:45:18 -07:00
|
|
|
$options = $this->getDiffFullOptions();
|
2011-01-09 15:22:25 -08:00
|
|
|
list($stdout) = execx(
|
|
|
|
"(cd %s; git diff {$options} %s -- %s)",
|
|
|
|
$this->getPath(),
|
|
|
|
$this->getRelativeCommit(),
|
|
|
|
$path);
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getBranchName() {
|
|
|
|
// TODO: consider:
|
|
|
|
//
|
|
|
|
// $ git rev-parse --abbrev-ref `git symbolic-ref HEAD`
|
|
|
|
//
|
|
|
|
// But that may fail if you're not on a branch.
|
|
|
|
list($stdout) = execx(
|
|
|
|
'(cd %s; git branch)',
|
|
|
|
$this->getPath());
|
|
|
|
|
|
|
|
$matches = null;
|
|
|
|
if (preg_match('/^\* (.+)$/m', $stdout, $matches)) {
|
|
|
|
return $matches[1];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSourceControlPath() {
|
|
|
|
// TODO: Try to get something useful here.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getGitCommitLog() {
|
|
|
|
$relative = $this->getRelativeCommit();
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
if ($this->repositoryHasNoCommits) {
|
|
|
|
// No commits yet.
|
|
|
|
return '';
|
|
|
|
} else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) {
|
|
|
|
// First commit.
|
2011-01-09 15:22:25 -08:00
|
|
|
list($stdout) = execx(
|
2011-03-07 15:37:38 -08:00
|
|
|
'(cd %s; git log --format=medium HEAD)',
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath());
|
|
|
|
} else {
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
// 2..N commits.
|
2011-01-09 15:22:25 -08:00
|
|
|
list($stdout) = execx(
|
2011-06-06 20:57:04 -07:00
|
|
|
'(cd %s; git log --first-parent --format=medium %s..HEAD)',
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath(),
|
|
|
|
$this->getRelativeCommit());
|
|
|
|
}
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getGitHistoryLog() {
|
|
|
|
list($stdout) = execx(
|
2011-03-07 15:37:38 -08:00
|
|
|
'(cd %s; git log --format=medium -n%d %s)',
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath(),
|
|
|
|
self::SEARCH_LENGTH_FOR_PARENT_REVISIONS,
|
|
|
|
$this->getRelativeCommit());
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSourceControlBaseRevision() {
|
|
|
|
list($stdout) = execx(
|
|
|
|
'(cd %s; git rev-parse %s)',
|
|
|
|
$this->getPath(),
|
|
|
|
$this->getRelativeCommit());
|
|
|
|
return rtrim($stdout, "\n");
|
|
|
|
}
|
|
|
|
|
2011-04-27 14:10:19 -07:00
|
|
|
/**
|
|
|
|
* Returns the sha1 of the HEAD revision
|
|
|
|
* @param boolean $short whether return the abbreviated or full hash.
|
|
|
|
*/
|
|
|
|
public function getGitHeadRevision($short=false) {
|
|
|
|
if ($short) {
|
|
|
|
$flags = '--short';
|
|
|
|
} else {
|
|
|
|
$flags = '';
|
|
|
|
}
|
2011-01-09 15:22:25 -08:00
|
|
|
list($stdout) = execx(
|
2011-04-27 14:10:19 -07:00
|
|
|
'(cd %s; git rev-parse %s HEAD)',
|
|
|
|
$this->getPath(),
|
|
|
|
$flags);
|
2011-01-09 15:22:25 -08:00
|
|
|
return rtrim($stdout, "\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getWorkingCopyStatus() {
|
|
|
|
if (!isset($this->status)) {
|
|
|
|
|
2011-05-29 10:45:18 -07:00
|
|
|
$options = $this->getDiffBaseOptions();
|
|
|
|
|
2011-07-05 11:06:46 -07:00
|
|
|
// -- parallelize these slow cpu bound git calls.
|
|
|
|
|
2011-01-09 15:22:25 -08:00
|
|
|
// Find committed changes.
|
2011-07-05 11:06:46 -07:00
|
|
|
$committed_future = new ExecFuture(
|
2011-05-29 10:45:18 -07:00
|
|
|
"(cd %s; git diff {$options} --raw %s --)",
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath(),
|
|
|
|
$this->getRelativeCommit());
|
|
|
|
|
|
|
|
// Find uncommitted changes.
|
2011-07-05 11:06:46 -07:00
|
|
|
$uncommitted_future = new ExecFuture(
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
"(cd %s; git diff {$options} --raw %s --)",
|
|
|
|
$this->getPath(),
|
|
|
|
$this->repositoryHasNoCommits
|
|
|
|
? self::GIT_MAGIC_ROOT_COMMIT
|
|
|
|
: 'HEAD');
|
2011-07-05 11:06:46 -07:00
|
|
|
|
|
|
|
// Untracked files
|
|
|
|
$untracked_future = new ExecFuture(
|
|
|
|
'(cd %s; git ls-files --others --exclude-standard)',
|
|
|
|
$this->getPath());
|
|
|
|
|
|
|
|
// TODO: This doesn't list unstaged adds. It's not clear how to get that
|
|
|
|
// list other than "git status --porcelain" and then parsing it. :/
|
|
|
|
|
|
|
|
// Unstaged changes
|
|
|
|
$unstaged_future = new ExecFuture(
|
|
|
|
'(cd %s; git ls-files -m)',
|
|
|
|
$this->getPath());
|
|
|
|
|
|
|
|
$futures = array(
|
|
|
|
$committed_future,
|
|
|
|
$uncommitted_future,
|
|
|
|
$untracked_future,
|
|
|
|
$unstaged_future
|
|
|
|
);
|
|
|
|
Futures($futures)->resolveAll();
|
|
|
|
|
|
|
|
|
|
|
|
// -- read back and process the results
|
|
|
|
|
|
|
|
list($stdout, $stderr) = $committed_future->resolvex();
|
|
|
|
$files = $this->parseGitStatus($stdout);
|
|
|
|
|
|
|
|
list($stdout, $stderr) = $uncommitted_future->resolvex();
|
2011-04-05 19:27:05 -07:00
|
|
|
$uncommitted_files = $this->parseGitStatus($stdout);
|
|
|
|
foreach ($uncommitted_files as $path => $mask) {
|
|
|
|
$mask |= self::FLAG_UNCOMMITTED;
|
|
|
|
if (!isset($files[$path])) {
|
|
|
|
$files[$path] = 0;
|
|
|
|
}
|
|
|
|
$files[$path] |= $mask;
|
|
|
|
}
|
2011-01-09 15:22:25 -08:00
|
|
|
|
2011-07-05 11:06:46 -07:00
|
|
|
list($stdout, $stderr) = $untracked_future->resolvex();
|
2011-01-09 15:22:25 -08:00
|
|
|
$stdout = rtrim($stdout, "\n");
|
|
|
|
if (strlen($stdout)) {
|
|
|
|
$stdout = explode("\n", $stdout);
|
|
|
|
foreach ($stdout as $file) {
|
|
|
|
$files[$file] = self::FLAG_UNTRACKED;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-07-05 11:06:46 -07:00
|
|
|
list($stdout, $stderr) = $unstaged_future->resolvex();
|
2011-01-09 15:22:25 -08:00
|
|
|
$stdout = rtrim($stdout, "\n");
|
|
|
|
if (strlen($stdout)) {
|
|
|
|
$stdout = explode("\n", $stdout);
|
|
|
|
foreach ($stdout as $file) {
|
2011-04-11 13:54:00 -07:00
|
|
|
$files[$file] = isset($files[$file])
|
|
|
|
? ($files[$file] | self::FLAG_UNSTAGED)
|
|
|
|
: self::FLAG_UNSTAGED;
|
2011-01-09 15:22:25 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->status = $files;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->status;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function amendGitHeadCommit($message) {
|
|
|
|
execx(
|
2011-12-19 16:12:13 -08:00
|
|
|
'(cd %s; git commit --amend --allow-empty --message %s)',
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath(),
|
|
|
|
$message);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getPreReceiveHookStatus($old_ref, $new_ref) {
|
2011-05-29 10:45:18 -07:00
|
|
|
$options = $this->getDiffBaseOptions();
|
2011-01-09 15:22:25 -08:00
|
|
|
list($stdout) = execx(
|
2011-05-29 10:45:18 -07:00
|
|
|
"(cd %s && git diff {$options} --raw %s %s --)",
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath(),
|
|
|
|
$old_ref,
|
|
|
|
$new_ref);
|
|
|
|
return $this->parseGitStatus($stdout, $full = true);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function parseGitStatus($status, $full = false) {
|
|
|
|
static $flags = array(
|
|
|
|
'A' => self::FLAG_ADDED,
|
|
|
|
'M' => self::FLAG_MODIFIED,
|
|
|
|
'D' => self::FLAG_DELETED,
|
|
|
|
);
|
|
|
|
|
|
|
|
$status = trim($status);
|
|
|
|
$lines = array();
|
|
|
|
foreach (explode("\n", $status) as $line) {
|
|
|
|
if ($line) {
|
|
|
|
$lines[] = preg_split("/[ \t]/", $line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$files = array();
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
$mask = 0;
|
|
|
|
$flag = $line[4];
|
|
|
|
$file = $line[5];
|
|
|
|
foreach ($flags as $key => $bits) {
|
|
|
|
if ($flag == $key) {
|
|
|
|
$mask |= $bits;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($full) {
|
|
|
|
$files[$file] = array(
|
|
|
|
'mask' => $mask,
|
|
|
|
'ref' => rtrim($line[3], '.'),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$files[$file] = $mask;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $files;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getBlame($path) {
|
|
|
|
// TODO: 'git blame' supports --porcelain and we should probably use it.
|
|
|
|
list($stdout) = execx(
|
2011-12-13 11:38:42 -08:00
|
|
|
'(cd %s; git blame --date=iso -w -M %s -- %s)',
|
2011-01-09 15:22:25 -08:00
|
|
|
$this->getPath(),
|
|
|
|
$this->getRelativeCommit(),
|
|
|
|
$path);
|
|
|
|
|
|
|
|
$blame = array();
|
|
|
|
foreach (explode("\n", trim($stdout)) as $line) {
|
|
|
|
if (!strlen($line)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// lines predating a git repo's history are blamed to the oldest revision,
|
|
|
|
// with the commit hash prepended by a ^. we shouldn't count these lines
|
|
|
|
// as blaming to the oldest diff's unfortunate author
|
|
|
|
if ($line[0] == '^') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$matches = null;
|
|
|
|
$ok = preg_match(
|
|
|
|
'/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/',
|
|
|
|
$line,
|
|
|
|
$matches);
|
|
|
|
if (!$ok) {
|
|
|
|
throw new Exception("Bad blame? `{$line}'");
|
|
|
|
}
|
|
|
|
$revision = $matches[1];
|
|
|
|
$author = $matches[2];
|
|
|
|
|
|
|
|
$blame[] = array($author, $revision);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $blame;
|
|
|
|
}
|
|
|
|
|
2011-01-11 13:02:38 -08:00
|
|
|
public function getOriginalFileData($path) {
|
|
|
|
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCurrentFileData($path) {
|
|
|
|
return $this->getFileDataAtRevision($path, 'HEAD');
|
|
|
|
}
|
|
|
|
|
|
|
|
private function parseGitTree($stdout) {
|
|
|
|
$result = array();
|
|
|
|
|
|
|
|
$stdout = trim($stdout);
|
|
|
|
if (!strlen($stdout)) {
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
$lines = explode("\n", $stdout);
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
$matches = array();
|
|
|
|
$ok = preg_match(
|
|
|
|
'/^(\d{6}) (blob|tree) ([a-z0-9]{40})[\t](.*)$/',
|
|
|
|
$line,
|
|
|
|
$matches);
|
|
|
|
if (!$ok) {
|
|
|
|
throw new Exception("Failed to parse git ls-tree output!");
|
|
|
|
}
|
|
|
|
$result[$matches[4]] = array(
|
|
|
|
'mode' => $matches[1],
|
|
|
|
'type' => $matches[2],
|
|
|
|
'ref' => $matches[3],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getFileDataAtRevision($path, $revision) {
|
|
|
|
|
|
|
|
// NOTE: We don't want to just "git show {$revision}:{$path}" since if the
|
|
|
|
// path was a directory at the given revision we'll get a list of its files
|
|
|
|
// and treat it as though it as a file containing a list of other files,
|
|
|
|
// which is silly.
|
|
|
|
|
|
|
|
list($stdout) = execx(
|
|
|
|
'(cd %s && git ls-tree %s -- %s)',
|
|
|
|
$this->getPath(),
|
|
|
|
$revision,
|
|
|
|
$path);
|
|
|
|
|
|
|
|
$info = $this->parseGitTree($stdout);
|
|
|
|
if (empty($info[$path])) {
|
|
|
|
// No such path, or the path is a directory and we executed 'ls-tree dir/'
|
|
|
|
// and got a list of its contents back.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($info[$path]['type'] != 'blob') {
|
|
|
|
// Path is or was a directory, not a file.
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
list($stdout) = execx(
|
|
|
|
'(cd %s && git cat-file blob %s)',
|
|
|
|
$this->getPath(),
|
|
|
|
$info[$path]['ref']);
|
|
|
|
return $stdout;
|
|
|
|
}
|
|
|
|
|
2011-06-23 12:10:59 -07:00
|
|
|
/**
|
|
|
|
* Returns names of all the branches in the current repository.
|
|
|
|
*
|
|
|
|
* @return array where each element is a triple ('name', 'sha1', 'current')
|
|
|
|
*/
|
|
|
|
public function getAllBranches() {
|
|
|
|
list($branch_info) = execx(
|
|
|
|
'cd %s && git branch --no-color', $this->getPath());
|
|
|
|
$lines = explode("\n", trim($branch_info));
|
|
|
|
$result = array();
|
|
|
|
foreach ($lines as $line) {
|
|
|
|
$match = array();
|
2011-06-29 13:21:30 -07:00
|
|
|
preg_match('/^(\*?)\s*(.*)$/', $line, $match);
|
|
|
|
$name = $match[2];
|
|
|
|
if ($name == '(no branch)') {
|
|
|
|
// Just ignore this, we could theoretically try to figure out the ref
|
|
|
|
// and treat it like a real branch but that's sort of ridiculous.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$result[] = array(
|
|
|
|
'current' => !empty($match[1]),
|
|
|
|
'name' => $name,
|
|
|
|
);
|
2011-06-23 12:10:59 -07:00
|
|
|
}
|
|
|
|
$all_names = ipull($result, 'name');
|
|
|
|
// Calling 'git branch' first and then 'git rev-parse' is way faster than
|
|
|
|
// 'git branch -v' for some reason.
|
|
|
|
list($sha1s_string) = execx(
|
2011-06-23 13:28:37 -07:00
|
|
|
"cd %s && git rev-parse %Ls",
|
|
|
|
$this->path,
|
|
|
|
$all_names);
|
2011-06-23 12:10:59 -07:00
|
|
|
$sha1_map = array_combine($all_names, explode("\n", trim($sha1s_string)));
|
|
|
|
foreach ($result as &$branch) {
|
|
|
|
$branch['sha1'] = $sha1_map[$branch['name']];
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns git commit messages for the given revisions,
|
|
|
|
* in the specified format (see git show --help for options).
|
|
|
|
*
|
|
|
|
* @param array $revs a list of commit hashes
|
|
|
|
* @param string $format the format to show messages in
|
|
|
|
*/
|
|
|
|
public function multigetCommitMessages($revs, $format) {
|
|
|
|
$delimiter = "%%x00";
|
|
|
|
$revs_list = implode(' ', $revs);
|
|
|
|
$show_command =
|
|
|
|
"git show -s --pretty=\"format:$format$delimiter\" $revs_list";
|
|
|
|
list($commits_string) = execx(
|
|
|
|
"cd %s && $show_command",
|
|
|
|
$this->getPath());
|
|
|
|
$commits_list = array_slice(explode("\0", $commits_string), 0, -1);
|
|
|
|
$commits_list = array_combine($revs, $commits_list);
|
|
|
|
return $commits_list;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getRepositoryOwner() {
|
|
|
|
list($owner) = execx(
|
|
|
|
'cd %s && git config --get user.name',
|
|
|
|
$this->getPath());
|
|
|
|
return trim($owner);
|
|
|
|
}
|
|
|
|
|
2011-12-02 16:21:14 -08:00
|
|
|
public function getWorkingCopyRevision() {
|
|
|
|
list($stdout) = execx(
|
|
|
|
'(cd %s; git rev-parse %s)',
|
|
|
|
$this->getPath(),
|
|
|
|
'HEAD');
|
|
|
|
return rtrim($stdout, "\n");
|
|
|
|
}
|
|
|
|
|
2011-09-14 18:44:54 -07:00
|
|
|
public function supportsRelativeLocalCommits() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function parseRelativeLocalCommit(array $argv) {
|
|
|
|
if (count($argv) == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (count($argv) != 1) {
|
|
|
|
throw new ArcanistUsageException("Specify only one commit.");
|
|
|
|
}
|
|
|
|
$base = reset($argv);
|
|
|
|
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
|
|
|
|
$merge_base = $base;
|
|
|
|
} else {
|
|
|
|
list($err, $merge_base) = exec_manual(
|
|
|
|
'(cd %s; git merge-base %s HEAD)',
|
|
|
|
$this->getPath(),
|
|
|
|
$base);
|
|
|
|
if ($err) {
|
|
|
|
throw new ArcanistUsageException(
|
Improve git behavior in the zero- and one- commit case
Summary:
Git works completely differently for commits zero and one than for 2..N so add
more special casing to handle them. See:
- {T206}
- {T596}
The getCommitRange() block is also fatal land, although I wasn't able to reach
it. I'll follow up with @s on T596.
Test Plan:
- Created a new, empty repository ("mkdir x; cd x; git init").
- Ran "arc lint", "arc unit", "arc diff" against it with no commits (the first
two work, the third fails helpfully).
- Made an initial commit.
- Ran "arc lint", "arc unit", "arc diff" against it (all work correctly).
Reviewers: btrahan, jungejason, aran
Reviewed By: aran
CC: s, aran
Differential Revision: 1142
2011-11-30 09:15:37 -08:00
|
|
|
"Unable to find any git commit named '{$base}' in this repository.");
|
2011-09-14 18:44:54 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->setRelativeCommit(trim($merge_base));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getAllLocalChanges() {
|
|
|
|
$diff = $this->getFullGitDiff();
|
|
|
|
$parser = new ArcanistDiffParser();
|
|
|
|
return $parser->parseDiff($diff);
|
|
|
|
}
|
|
|
|
|
Add an "arc merge" workflow
Summary:
This should support conservative rewrite policies in git fairly well, under an
assumed workflow of:
- Develop in local branches, never rewrite history.
- Commit with "-m" or by typing a brief, non-template commit message
describing the checkpoint.
- Provide rich information in the web console (reviewers, etc.)
- Finalize with "git checkout master && arc merge branch && git push" or some
flavor thereof.
This supports Mercurial somewhat. The major problem is that "hg merge" fails if
the local is a fastforward of the remote, at which point there's nowhere we can
throw the commit message. Oh well. Just push it and we'll do our best to link
them up based on local commit info.
I am increasingly forming an opinion that Mercurial is "saftey-scissors git".
But also maybe I have no clue what I'm doing. I just don't understand why anyone
would think it's a good idea to have a trunk consisting of ~50% known-broken
revisions, random checkpoint parts, whitespace changes, typo fixes, etc. If you
use git with branching you can avoid this by making a trunk out of merges or
with rebase/amend, but there seems to be no way to have "one commit = one idea"
in any real sense in Mercurial.
Test Plan: Execute "arc merge" in git and mercurial.
Reviewers: fratrik, Makinde, aran, jungejason, tuomaspelkonen
Reviewed By: Makinde
CC: aran, epriestley, Makinde
Differential Revision: 860
2011-08-25 16:02:03 -07:00
|
|
|
public function supportsLocalBranchMerge() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function performLocalBranchMerge($branch, $message) {
|
|
|
|
if (!$branch) {
|
|
|
|
throw new ArcanistUsageException(
|
|
|
|
"Under git, you must specify the branch you want to merge.");
|
|
|
|
}
|
|
|
|
$err = phutil_passthru(
|
|
|
|
'(cd %s && git merge --no-ff -m %s %s)',
|
|
|
|
$this->getPath(),
|
|
|
|
$message,
|
|
|
|
$branch);
|
|
|
|
|
|
|
|
if ($err) {
|
|
|
|
throw new ArcanistUsageException("Merge failed!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getFinalizedRevisionMessage() {
|
|
|
|
return "You may now push this commit upstream, as appropriate (e.g. with ".
|
|
|
|
"'git push', or 'git svn dcommit', or by printing and faxing it).";
|
|
|
|
}
|
|
|
|
|
2012-01-12 19:03:11 -08:00
|
|
|
public function getCommitMessageForRevision($rev) {
|
|
|
|
list($message) = execx(
|
|
|
|
'(cd %s && git log -n1 %s)',
|
|
|
|
$this->getPath(),
|
|
|
|
$rev);
|
|
|
|
$parser = new ArcanistDiffParser();
|
|
|
|
return head($parser->parseDiff($message));
|
|
|
|
}
|
|
|
|
|
2012-01-24 08:07:38 -08:00
|
|
|
public function loadWorkingCopyDifferentialRevisions(
|
|
|
|
ConduitClient $conduit,
|
|
|
|
array $query) {
|
|
|
|
|
|
|
|
$messages = $this->getGitCommitLog();
|
|
|
|
if (!strlen($messages)) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
$parser = new ArcanistDiffParser();
|
|
|
|
$messages = $parser->parseDiff($messages);
|
|
|
|
|
|
|
|
// First, try to find revisions by explicit revision IDs in commit messages.
|
|
|
|
$revision_ids = array();
|
|
|
|
foreach ($messages as $message) {
|
|
|
|
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus(
|
|
|
|
$message->getMetadata('message'));
|
|
|
|
if ($object->getRevisionID()) {
|
|
|
|
$revision_ids[] = $object->getRevisionID();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($revision_ids) {
|
|
|
|
$results = $conduit->callMethodSynchronous(
|
|
|
|
'differential.query',
|
|
|
|
$query + array(
|
|
|
|
'ids' => $revision_ids,
|
|
|
|
));
|
|
|
|
return $results;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we didn't succeed, try to find revisions by hash.
|
|
|
|
$hashes = array();
|
|
|
|
foreach ($this->getLocalCommitInformation() as $commit) {
|
|
|
|
$hashes[] = array('gtcm', $commit['commit']);
|
|
|
|
$hashes[] = array('gttr', $commit['tree']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$results = $conduit->callMethodSynchronous(
|
|
|
|
'differential.query',
|
|
|
|
$query + array(
|
|
|
|
'commitHashes' => $hashes,
|
|
|
|
));
|
|
|
|
|
|
|
|
if ($results) {
|
|
|
|
return $results;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we still didn't succeed, try to find revisions by branch name.
|
|
|
|
$results = $conduit->callMethodSynchronous(
|
|
|
|
'differential.query',
|
|
|
|
$query + array(
|
|
|
|
'branches' => array($this->getBranchName()),
|
|
|
|
));
|
|
|
|
|
|
|
|
return $results;
|
|
|
|
}
|
|
|
|
|
2011-01-09 15:22:25 -08:00
|
|
|
}
|