diff --git a/src/internationalization/ArcanistUSEnglishTranslation.php b/src/internationalization/ArcanistUSEnglishTranslation.php --- a/src/internationalization/ArcanistUSEnglishTranslation.php +++ b/src/internationalization/ArcanistUSEnglishTranslation.php @@ -68,6 +68,16 @@ 'Ignore this untracked file and continue?', 'Ignore these untracked files and continue?', ), + + '%s submodule(s) have uncommitted or untracked changes:' => array( + 'A submodule has uncommitted or untracked changes:', + 'Submodules have uncommitted or untracked changes:', + ), + + 'Ignore the changes to these %s submodule(s) and continue?' => array( + 'Ignore the changes to this submodule and continue?', + 'Ignore the changes to these submodules and continue?', + ), ); } diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php --- a/src/repository/api/ArcanistGitAPI.php +++ b/src/repository/api/ArcanistGitAPI.php @@ -672,7 +672,7 @@ $result = new PhutilArrayWithDefaultValue(); list($stdout) = $uncommitted_future->resolvex(); - $uncommitted_files = $this->parseGitStatus($stdout); + $uncommitted_files = $this->parseGitRawDiff($stdout); foreach ($uncommitted_files as $path => $mask) { $result[$path] |= ($mask | self::FLAG_UNCOMMITTED); } @@ -704,7 +704,7 @@ $this->getDiffBaseOptions(), $this->getBaseCommit()); - return $this->parseGitStatus($stdout); + return $this->parseGitRawDiff($stdout); } public function getGitConfig($key, $default = null) { @@ -759,7 +759,7 @@ return $this; } - private function parseGitStatus($status, $full = false) { + private function parseGitRawDiff($status, $full = false) { static $flags = array( 'A' => self::FLAG_ADDED, 'M' => self::FLAG_MODIFIED, @@ -777,17 +777,51 @@ $files = array(); foreach ($lines as $line) { $mask = 0; + + // "git diff --raw" lines begin with a ":" character. + $old_mode = ltrim($line[0], ':'); + $new_mode = $line[1]; + + // The hashes may be padded with "." characters for alignment. Discard + // them. + $old_hash = rtrim($line[2], '.'); + $new_hash = rtrim($line[3], '.'); + $flag = $line[4]; $file = $line[5]; - foreach ($flags as $key => $bits) { - if ($flag == $key) { - $mask |= $bits; - } + + $new_value = intval($new_mode, 8); + $is_submodule = (($new_value & 0160000) === 0160000); + + if (($is_submodule) && + ($flag == 'M') && + ($old_hash === $new_hash) && + ($old_mode === $new_mode)) { + // See T9455. We see this submodule as "modified", but the old and new + // hashes are the same and the old and new modes are the same, so we + // don't directly see a modification. + + // We can end up here if we have a submodule which has uncommitted + // changes inside of it (for example, the user has added untracked + // files or made uncommitted changes to files in the submodule). In + // this case, we set a different flag because we can't meaningfully + // give users the same prompt. + + // Note that if the submodule has real changes from the parent + // perspective (the base commit has changed) and also has uncommitted + // changes, we'll only see the real changes and miss the uncommitted + // changes. At the time of writing, there is no reasonable porcelain + // for finding those changes, and the impact of this error seems small. + + $mask |= self::FLAG_EXTERNALS; + } else if (isset($flags[$flag])) { + $mask |= $flags[$flag]; } + if ($full) { $files[$file] = array( 'mask' => $mask, - 'ref' => rtrim($line[3], '.'), + 'ref' => $new_hash, ); } else { $files[$file] = $mask; @@ -807,7 +841,7 @@ list($stdout) = $this->execxLocal( 'diff --raw %s', $since_commit); - return $this->parseGitStatus($stdout); + return $this->parseGitRawDiff($stdout); } public function getBlame($path) { diff --git a/src/repository/api/ArcanistRepositoryAPI.php b/src/repository/api/ArcanistRepositoryAPI.php --- a/src/repository/api/ArcanistRepositoryAPI.php +++ b/src/repository/api/ArcanistRepositoryAPI.php @@ -15,6 +15,9 @@ const FLAG_MISSING = 32; const FLAG_UNSTAGED = 64; const FLAG_UNCOMMITTED = 128; + + // Occurs in SVN when you have uncommitted changes to a modified external, + // or in Git when you have uncommitted or untracked changes in a submodule. const FLAG_EXTERNALS = 256; // Occurs in SVN when you replace a file with a directory without telling @@ -195,6 +198,14 @@ /** * @task status */ + final public function getDirtyExternalChanges() { + return $this->getUncommittedPathsWithMask(self::FLAG_EXTERNALS); + } + + + /** + * @task status + */ private function getUncommittedPathsWithMask($mask) { $match = array(); foreach ($this->getUncommittedStatus() as $path => $flags) { diff --git a/src/workflow/ArcanistFeatureWorkflow.php b/src/workflow/ArcanistFeatureWorkflow.php --- a/src/workflow/ArcanistFeatureWorkflow.php +++ b/src/workflow/ArcanistFeatureWorkflow.php @@ -356,8 +356,8 @@ foreach ($out as $line) { $table->addRow(array( 'current' => $line['current'] ? '*' : '', - 'name' => phutil_console_format('**%s**', $line['name']), - 'status' => phutil_console_format( + 'name' => tsprintf('**%s**', $line['name']), + 'status' => tsprintf( "%s", $line['status']), 'descr' => $line['desc'], )); diff --git a/src/workflow/ArcanistListWorkflow.php b/src/workflow/ArcanistListWorkflow.php --- a/src/workflow/ArcanistListWorkflow.php +++ b/src/workflow/ArcanistListWorkflow.php @@ -91,11 +91,11 @@ $revision = $revisions[$key]; $table->addRow(array( - 'exists' => $spec['exists'] ? phutil_console_format('**%s**', '*') : '', - 'status' => phutil_console_format( + 'exists' => $spec['exists'] ? tsprintf('**%s**', '*') : '', + 'status' => tsprintf( "%s", $spec['statusName']), - 'title' => phutil_console_format( + 'title' => tsprintf( '**D%d:** %s', $revision['id'], $revision['title']), diff --git a/src/workflow/ArcanistPhrequentWorkflow.php b/src/workflow/ArcanistPhrequentWorkflow.php --- a/src/workflow/ArcanistPhrequentWorkflow.php +++ b/src/workflow/ArcanistPhrequentWorkflow.php @@ -56,7 +56,7 @@ $table->addRow(array( 'type' => '('.$column_type.')', - 'time' => phutil_format_relative_time($result['time']), + 'time' => tsprintf($result['time']), 'name' => $phid_map[$result['phid']], )); diff --git a/src/workflow/ArcanistTasksWorkflow.php b/src/workflow/ArcanistTasksWorkflow.php --- a/src/workflow/ArcanistTasksWorkflow.php +++ b/src/workflow/ArcanistTasksWorkflow.php @@ -111,7 +111,7 @@ // Render the "T123" column. $task_id = 'T'.$task['id']; - $formatted_task_id = phutil_console_format('**%s**', $task_id); + $formatted_task_id = tsprintf('**%s**', $task_id); $output['id'] = $formatted_task_id; // Render the "Title" column. @@ -145,7 +145,7 @@ } else { $color = 'white'; } - $formatted_priority = phutil_console_format( + $formatted_priority = tsprintf( " %s", $task['priority']); $output['priority'] = $formatted_priority; @@ -159,7 +159,7 @@ $status_text = $task['statusName']; $status_color = 'green'; } - $formatted_status = phutil_console_format( + $formatted_status = tsprintf( " %s", $status_text); $output['status'] = $formatted_status; diff --git a/src/workflow/ArcanistWorkflow.php b/src/workflow/ArcanistWorkflow.php --- a/src/workflow/ArcanistWorkflow.php +++ b/src/workflow/ArcanistWorkflow.php @@ -893,11 +893,47 @@ implode("\n ", $missing))); } + $externals = $api->getDirtyExternalChanges(); + + // TODO: This state can exist in Subversion, but it is currently handled + // elsewhere. It should probably be handled here, eventually. + if ($api instanceof ArcanistSubversionAPI) { + $externals = array(); + } + + if ($externals) { + $message = pht( + '%s submodule(s) have uncommitted or untracked changes:', + new PhutilNumber(count($externals))); + + $prompt = pht( + 'Ignore the changes to these %s submodule(s) and continue?', + new PhutilNumber(count($externals))); + + $list = id(new PhutilConsoleList()) + ->setWrap(false) + ->addItems($externals); + + id(new PhutilConsoleBlock()) + ->addParagraph($message) + ->addList($list) + ->draw(); + + $ok = phutil_console_confirm($prompt, $default_no = false); + if (!$ok) { + throw new ArcanistUserAbortException(); + } + } + $uncommitted = $api->getUncommittedChanges(); $unstaged = $api->getUnstagedChanges(); + // We already dealt with externals. + $unstaged = array_diff($unstaged, $externals); + // We only want files which are purely uncommitted. $uncommitted = array_diff($uncommitted, $unstaged); + $uncommitted = array_diff($uncommitted, $externals); $untracked = $api->getUntrackedChanges(); if (!$this->shouldRequireCleanUntrackedFiles()) {