From 2e0bd98d3824bb222096dfa57367af51148f4843 Mon Sep 17 00:00:00 2001
From: Greg Anderson <greg.1.anderson@greenknowe.org>
Date: Sat, 11 Aug 2012 20:59:53 -0700
Subject:  Greg Anderson: Generated with Drush iq

---
 commands/core/docs.drush.inc |    8 +
 commands/iq/iq.drush.inc     |  949 ++++++++++++++++++++++++++++++++++++++++++
 docs/iq-commands.html        |  106 +++++
 includes/command.inc         |    1 +
 tests/iqTest.php             |   38 ++
 5 files changed, 1102 insertions(+)
 create mode 100644 commands/iq/iq.drush.inc
 create mode 100644 docs/iq-commands.html
 create mode 100644 tests/iqTest.php

diff --git a/commands/core/docs.drush.inc b/commands/core/docs.drush.inc
index 91e12dd..981ea51 100644
--- a/commands/core/docs.drush.inc
+++ b/commands/core/docs.drush.inc
@@ -191,6 +191,14 @@ function docs_drush_command() {
     'callback' => 'drush_print_file',
     'callback arguments' => array($docs_dir . '/docs/strict-options.html'),
   );
+  $items['docs-iq-commands'] = array(
+    'description' => 'Issue queue commands for applying and creating patches from drupal.org.',
+    'hidden' => TRUE,
+    'topic' => TRUE,
+    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
+    'callback' => 'drush_print_file',
+    'callback arguments' => array($docs_dir . '/docs/iq-commands.html'),
+  );
   return $items;
 }
 
diff --git a/commands/iq/iq.drush.inc b/commands/iq/iq.drush.inc
new file mode 100644
index 0000000..64031ae
--- /dev/null
+++ b/commands/iq/iq.drush.inc
@@ -0,0 +1,949 @@
+<?php
+
+/**
+ * @file
+ *  The drush Issue Queue manager
+ */
+
+
+
+/**
+ * Implementation of hook_drush_command().
+ */
+function iq_drush_command() {
+  $items['iq-info'] = array(
+    'description' => 'Show information about an issue from the queue on drupal.org.',
+    'examples' => array(
+      'drush iq-info 1234' => 'Get info on issue 1234.',
+      'drush iq-info http://drupal.org/node/1234' => 'Get info on an issue identified by its URL.',
+    ),
+    'arguments' => array(
+      'number' => 'The issue number.',
+    ),
+    'required-arguments' => TRUE,
+    'options' => array(
+      'pipe' => 'Print the full issue info data structure.',
+    ),
+    'aliases' => array('iqi'),
+    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
+  );
+  $items['iq-create-commit-comment'] = array(
+    'description' => 'Create a commit comment for the specified issue number.',
+    'examples' => array(
+      'drush iq-create-commit-comment 1234' => 'Generate a commit comment using the issue title, and crediting every user who provided attachments.',
+    ),
+    'arguments' => array(
+      'number' => 'The issue number.',
+    ),
+    'aliases' => array('iqccc', 'ccc'),
+    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
+  );
+  $items['iq-apply-patch'] = array(
+    'description' => 'Look up the most recent patch attached to the specified issue, and apply it to its project.',
+    'examples' => array(
+      'drush iq-apply-patch 1234' => 'Apply the newest patch attached to issue 1234.',
+      'drush iq-apply-patch 1234-5' => 'Apply the patch attached to the fifth comment of issue 1234.',
+      'drush iq-apply-patch 1234-5678' => 'Apply the patch attached to comment id 5678 of issue 1234.',
+      'drush iq-apply-patch 1234 --select' => 'Show all patches attached to issue 1234, and prompt for the one to apply.',
+    ),
+    'arguments' => array(
+      'number' => 'The issue number.',
+    ),
+    'required-arguments' => TRUE,
+    'options' => array(
+      'no-prefix' => 'Patch was created with --no-prefix, and therefore should be applied with -Np0 instead of -Np1.  Optional; default is to try both.',
+      'no-git' => 'Do not execute any git commands. Default is to create a new branch for the issue.',
+      'keep-patch' => 'Keep the patchfile after applying it.  Default is to delete the patchfile.',
+      'select' => 'Prompt for which patch to apply.  Optional; default is newest patch.',
+    ),
+    'aliases' => array('patch', 'ap'),
+    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
+    'topics' => array('docs-iq-commands'),
+  );
+  $items['iq-diff'] = array(
+    'description' => 'Create a diff.  Uses `git format patch`, so author information is included.',
+    'examples' => array(
+      'drush iq-diff' => 'Create a diff between the current branch and the branch it split from.',
+      'drush iq-diff --commit' => 'Commit the changes to the current branch after creating the diff.',
+    ),
+    'options' => array(
+      'no-prefix' => 'Create patch with no prefix.  Not recommended; patch will have to be applied with -Np0 instead of -Np1.',
+      'no-git' => 'Do not execute any git commands; just run diff. Default is to use git format-patch.',
+      'commit' => 'Commit change to branch. Optional; default is to leave changes unstaged.',
+      'no-squash' => 'Show all commits in the branch, like a standard format-patch. Default is to squash into a single commit.',
+      'rebase' => 'Rebase before creating patch.',
+      'message' => 'Commit message.  Optional; default is to use issue title.',
+    ),
+    'aliases' => array('diff', 'iqd'),
+    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
+    'topics' => array('docs-iq-commands'),
+  );
+  $items['iq-reset'] = array(
+    'description' => 'Stop working on a patch, and return to the branch point.',
+    'examples' => array(
+      'drush iq-reset' => 'Safely go back to the initial branch.',
+      'drush iq-reset --delete' => 'Permanently delete all changes, and go back to the initial branch.',
+    ),
+    'options' => array(
+      'delete' => 'Also delete the working branch.',
+    ),
+    'aliases' => array('reset', 'iqr'),
+    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
+    'topics' => array('docs-iq-commands'),
+  );
+
+  return $items;
+}
+
+function drush_iq_diff($number = NULL) {
+  $branch_merges_with = FALSE;
+  $issue_info = array();
+  $remote = FALSE;
+  if (isset($number)) {
+    $issue_info = drush_iq_get_info($number);
+    if (!$issue_info) {
+      return FALSE;
+    }
+    $branch = _drush_iq_get_branch($issue_info);
+    $dir = _drush_iq_project_dir($issue_info);
+    if (!$dir) {
+      return drush_set_error('DRUSH_IQ_DIFF_NO_PROJECT', dt('Could not find the project directory for !n', array('!n' => $number)));
+    }
+  }
+  else {
+    $dir = drush_get_context('DRUSH_OLDCWD', drush_cwd());
+    $branch = _drush_iq_get_branch_at_dir($dir);
+    $branch_merges_with = _drush_iq_get_branch_merges_with($dir);
+    if ($branch_merges_with === FALSE) {
+      return FALSE;
+    }
+    if (!empty($branch_merges_with)) {
+      $remote = _drush_iq_get_best_remote($branch_merges_with);
+      $branch_merges_with = _drush_iq_merge_branch_with_remote($remote, $branch_merges_with);
+    }
+  }
+  if (!$branch_merges_with) {
+    $branch_merges_with = 'master';
+  }
+
+  // Not under git revision control is an error
+  if (empty($branch)) {
+    return drush_set_error('DRUSH_NO_VCS', dt("Error: drush can only produce diffs of projects under git version control"));
+  }
+
+  // If the user did not provide an issue number, but we can find
+  // that a git branch has been made at the current working directory,
+  // then we will expect that the branch tag is in the format of
+  // "arbitrary-prequel-ISSUENUMBER".  If it is in this form, drush_iq_get_info
+  // will be able to find the issue number.
+  if (isset($branch) && ($branch != "master") && !isset($issue_info)) {
+    list($number, $comment_number) = drush_iq_issue_number($branch);
+    if ($number) {
+      $issue_info = drush_iq_download_info($number, $comment_number);
+    }
+  }
+  // Add on extra flags
+  $extra = "";
+  if (drush_get_option('no-prefix', FALSE)) {
+    $extra .= ' --no-prefix';
+  }
+
+  $cwd = getcwd();
+  $committed = FALSE;
+  $squashed = FALSE;
+  drush_op('chdir', $dir);
+  $patch_description = "drush-iq";
+  if (!empty($issue_info)) {
+    $comment_number = 1 + count($issue_info['comments']);
+    $patch_description = _drush_iq_make_description($issue_info['title']) . '-' . $issue_info['id'] . '-' . $comment_number . '.patch';
+  }
+  drush_log(dt("# Create patch: !patch_description (branch: !b merge: !m)", array('!patch_description' => $patch_description, '!b' => $branch, '!m' => $branch_merges_with)), 'notice');
+
+  // If we have modified the master branch (not recommended), just run git diff
+  if (($branch == "master") || ($branch == $branch_merges_with) || drush_get_option('no-git', FALSE)) {
+    $result = drush_shell_exec_interactive("git diff $extra %s", $branch_merges_with);
+  }
+  // Changes are being made under a local branch.  Do the recommended procedure for creating the diff.
+  else {
+    $commit_comment = _drush_iq_create_commit_comment($issue_info, drush_get_option('committer', TRUE), drush_get_option('message', FALSE));
+    $result = drush_shell_exec("git status -s");
+    $status_output = drush_shell_exec_output();
+    // If there are unstaged changes, commit them
+    if (!empty($status_output)) {
+      // TODO: can we easily ignore *.patch, etc., or easily add only modified files?
+      $result = drush_shell_exec("git add .");
+      $result = drush_shell_exec("git commit -m %s", $commit_comment);
+      if (!$result) {
+        return drush_set_error('DRUSH_IQ_APPLY_PATCH_CANNOT_COMMIT', dt("Commit failed."));
+      }
+      $committed = TRUE;
+    }
+    // If the branch to merge with is remote, then fetch and rebase
+    if (drush_get_option('rebase', FALSE)) {
+      if ($remote) {
+        drush_shell_exec("git fetch %s %s && git rebase %s", $remote, $branch, $branch_merges_with);
+      }
+      else {
+        drush_log(dt('--rebase ignored because !branch has no upstream branch.', array('!branch' => $branch)), 'warning');
+      }
+    }
+
+    // Check to see if there are multiple commits on this branch. If there
+    // are, make a new temporary branch and squash-merge all commits there.
+    if (!drush_get_option('no-squash', FALSE)) {
+      $result = drush_shell_exec("git log --oneline %s..HEAD", $branch_merges_with);
+      $commits_output = drush_shell_exec_output();
+      if (count($commits_output) > 1) {
+        drush_shell_exec('git branch -D drush-iq-temp 2>/dev/null');
+        drush_shell_exec('git checkout %s', $branch_merges_with);
+        drush_shell_exec('git checkout -b drush-iq-temp');
+        drush_shell_exec('git merge --squash %s', $branch);
+        drush_shell_exec("git commit -m %s", $commit_comment);
+        $squashed = TRUE;
+      }
+    }
+
+    $result = drush_shell_exec_interactive("git format-patch %s -k $extra --stdout", $branch_merges_with);
+    if ($squashed) {
+      drush_shell_exec('git checkout %s', $branch);
+      drush_shell_exec('git branch -D drush-iq-temp  2>/dev/null');
+    }
+    if ($committed && !drush_get_option('commit', FALSE)) {
+      // Get rid of our commit
+      drush_shell_exec("git reset HEAD~1");
+    }
+  }
+  drush_op('chdir', $cwd);
+
+  return $branch;
+}
+
+/**
+ * iq-info command callback
+ */
+function drush_iq_info($number = NULL) {
+  if (isset($number)) {
+    $issue_info = drush_iq_get_info($number);
+  }
+  else {
+    $dir = drush_get_context('DRUSH_OLDCWD', drush_cwd());
+    $branch = _drush_iq_get_branch_at_dir($dir);
+    $issue_info = drush_iq_get_info($branch);
+  }
+  if ($issue_info === FALSE) {
+    return FALSE;
+  }
+
+  $label_map = _drush_iq_label_map();
+
+  if (drush_get_option('pipe')) {
+    drush_print_pipe(var_export($issue_info, TRUE));
+  }
+  else {
+    $rows = array();
+
+    foreach($label_map as $label => $key) {
+      $rows[] = array(substr($label, 0, -1), ':', $issue_info[$key]);
+    }
+
+    drush_print_table($rows);
+  }
+  return $issue_info;
+}
+
+/**
+ * iq-create-commit-comment command callback
+ */
+function drush_iq_create_commit_comment($number = NULL) {
+  if (isset($number)) {
+    $issue_info = drush_iq_get_info($number);
+  }
+  else {
+    $dir = drush_get_context('DRUSH_OLDCWD', drush_cwd());
+    $branch = _drush_iq_get_branch_at_dir($dir);
+    $issue_info = drush_iq_get_info($branch);
+  }
+  if ($issue_info === FALSE) {
+    return FALSE;
+  }
+
+  $result = _drush_iq_create_commit_comment($issue_info, drush_get_option('committer', FALSE));
+  drush_print($result);
+  return $result;
+}
+
+/**
+ * Abandon the current branch
+ */
+function drush_iq_reset($number = NULL) {
+  $cwd = getcwd();
+  if (isset($number)) {
+    $issue_info = drush_iq_get_info($number);
+    if (!$issue_info) {
+      return FALSE;
+    }
+    $dir = _drush_iq_project_dir($issue_info);
+    if (!$dir) {
+      return drush_set_error('DRUSH_IQ_RESET_NO_PROJECT', dt('Could not find the project directory for !n', array('!n' => $number)));
+    }
+  }
+  else {
+    $dir = drush_get_context('DRUSH_OLDCWD', drush_cwd());
+  }
+  $result = drush_shell_cd_and_exec($dir, "git status -s");
+  $status_output = drush_shell_exec_output();
+  $unstaged_changes = (!empty($status_output));
+  $branch = _drush_iq_get_branch_at_dir($dir);
+  $branch_merges_with = _drush_iq_get_branch_merges_with($dir);
+  if ($branch_merges_with === FALSE) {
+    return FALSE;
+  }
+  if (empty($branch_merges_with)) {
+    $branch_merges_with = 'master';
+  }
+  $delete = drush_get_option('delete', FALSE);
+
+  $reset_message = dt("Would you like to reset to the branch !merges_with? ", array('!merges_with' => $branch_merges_with, '!branch' => $branch));
+
+  if (($branch == '(no branch)') || ($branch_merges_with == $branch)) {
+    $reset_message = "";
+    if (!$unstaged_changes) {
+      return drush_set_error('DRUSH_IQ_NO_UNSTAGED_CHANGES', dt("No unstaged changes, and not on a branch; Drush cannot reset. If you have commits that you would like to get rid of, try:\n  git reset HEAD~1\nReplace '1' with the number of commits you have made. Use 'git log' and 'git status' for information."));
+    }
+    if (!$delete) {
+      return drush_set_error('DRUSH_WILL_NOT_DELETE', dt("Not on a branch; Drush will not delete your unstaged changes unless you use the --delete flag."));
+    }
+    $action_message = dt("Your unstaged changes will be permanently deleted.");
+  }
+  elseif ($unstaged_changes) {
+    $action_message = $delete ? dt("Your working branch !branch will be deleted, along with all unstaged changes.", array('!merges_with' => $branch_merges_with, '!branch' => $branch)) : dt("Your work will be preserved; you can return to it by typing:\n    git checkout !branch\nYour unstaged changes will be re-applied on top of the branch !merges_with", array('!merges_with' => $branch_merges_with, '!branch' => $branch));
+  }
+  else {
+    $action_message = $delete ? dt("Your working branch !branch will be deleted.", array('!merges_with' => $branch_merges_with, '!branch' => $branch)) : dt("Your work will be preserved; you can return to it by typing:\n    git checkout !branch", array('!merges_with' => $branch_merges_with, '!branch' => $branch));
+  }
+  drush_print($reset_message . $action_message);
+  $confirm = drush_confirm(dt("Is it okay to continue?"));
+  if (!$confirm) {
+    return drush_user_abort();
+  }
+
+  if ($delete) {
+    drush_shell_exec_interactive("git reset --hard HEAD");
+  }
+  if ($branch_merges_with != $branch) {
+    drush_op('chdir', $dir);
+    $result = drush_shell_exec_interactive("git checkout %s", $branch_merges_with);
+    if ($delete && ($branch != '(no branch)')) {
+      $result = drush_shell_exec_interactive("git branch -D %s", $branch);
+    }
+  }
+  drush_op('chdir', $cwd);
+}
+
+/**
+ * iq-apply-patch command callback
+ *
+ * Given an issue number, find the most recent patch file
+ * attached to it
+ */
+function drush_iq_apply_patch($number) {
+  $issue_info = drush_iq_get_info($number);
+  if (!$issue_info) {
+    return FALSE;
+  }
+  $patches = _drush_iq_find_patches($issue_info);
+  $do_git_operations = !drush_get_option('no-git', FALSE);
+  $result = FALSE;
+
+  if (empty($patches)) {
+    return drush_set_error('DRUSH_NO_PATCHES', dt("Could not find a patch in !issue", array('!issue' => _drush_iq_create_commit_comment($issue_info))));
+  }
+
+  // If --select, then prompt the user
+  if (drush_get_option('select', FALSE)) {
+    $choices = array();
+    foreach ($patches as $index => $url) {
+      $choices[$url] = array("#$index", ":", $url);
+    }
+    $patch = drush_choice($choices, dt("Select a patch to apply:"));
+    if ($patch === FALSE) {
+      return drush_user_abort();
+    }
+  }
+  // If the issue specification included a comment number, e.g. #1078108-1, then select the patch attached to the specified comment
+  elseif (array_key_exists('comment-number', $issue_info)) {
+    $index = $issue_info['comment-number'];
+    if (!array_key_exists($index, $patches)) {
+      var_export($patches);
+      return drush_set_error('DRUSH_IQ_NO_PATCH', dt("Error: comment #!index does not exist or does not have a patch.", array('!index' => $index)));
+    }
+    $patch = $patches[$index];
+  }
+  else {
+    // TODO: See http://drupal.org/node/1078108#comment-5659936 and
+    // http://drupal.org/node/1078108#comment-5663932 for additional
+    // heuristics we might apply here.
+    $patch = array_pop($patches);
+  }
+
+  $project_dir = _drush_iq_project_dir($issue_info);
+  if (!$project_dir) {
+    return drush_set_error('DRUSH_IQ_APPLY_PATCH_NO_PROJECT', dt('You are not working in a site, and the current directory is not named after the project.  Please cd to the project !project and try again.', array('!project' => $issue_info['projectName'])));
+  }
+  drush_log(dt("Downloading patchfile !patch for project !project", array('!patch' => basename($patch),'!project' => $issue_info['projectName'])), 'ok');
+  // n.b. the file returned by drush_download_file is either in the cache,
+  // or is registered for deletion
+  $filename = drush_download_file($patch);
+  if (!file_exists($filename)) {
+    return drush_set_error('DRUSH_PATCH_NOT_DOWNLOADED', dt("Patch could not be downloaded."));
+  }
+  $filename = realpath($filename);
+  $handle = @fopen($filename, "r");
+  if (!$handle) {
+    return drush_set_error('DRUSH_PATCH_NOT_READABLE', dt("Patch could not be read."));
+  }
+  // Get the first 80 characters or so from the file; we really only
+  // care whether it starts 'diff' or 'from'.
+  $line = fgets($handle, 80);
+  $from_format_patch = (strncasecmp('from ', $line, 5) == 0);
+  $from_diff = (strncasecmp('diff ', $line, 5) == 0);
+  if (!$from_format_patch && !$from_diff) {
+    drush_log("Could not tell if patch was created by diff or git format-patch; will try both.");
+    $from_format_patch = $from_diff = TRUE;
+  }
+  fclose($handle);
+
+  $keep_patch = FALSE;
+  if (drush_get_option('keep-patch', FALSE)) {
+    $keep_patch = dirname($filename) . '/iq-' . basename($filename);
+    copy($filename, $keep_patch);
+  }
+
+  $cwd = getcwd();
+  drush_op('chdir', $project_dir);
+
+  $try_patch_tool = TRUE;
+  if ($do_git_operations && $from_format_patch) {
+    $branch_merges_with = _drush_iq_get_branch_merges_with($project_dir);
+    if (!$branch_merges_with) {
+      $branch_merges_with = _drush_iq_get_branch_at_dir($project_dir);
+      if (empty($branch_merges_with)) {
+        $result = drush_shell_cd_and_exec($project_dir, "git init");
+        $result = drush_shell_cd_and_exec($project_dir, "git add .");
+        if (dirname($filename) == $project_dir) {
+          $result = drush_shell_cd_and_exec($project_dir, "git rm --cached -- %s", basename($filename));
+        }
+        if ($keep_patch) {
+          $result = drush_shell_cd_and_exec($project_dir, "git rm --cached -- %s", basename($keep_patch));
+        }
+        $result = drush_shell_cd_and_exec($project_dir, "git commit -m 'Git repository created by drush apply-patch command.'");
+      }
+    }
+    else {
+      $branch_merges_with = 'origin/' . $branch_merges_with;
+    }
+    drush_log(dt("Starting at branch !branchlabel", array('!branchlabel' => $branch_merges_with)), 'notice');
+
+    // Make a branch via 'git checkout -b [description]-[issue]'
+    $description = $issue_info['title'];
+    $branchlabel = 'drush-iq-' . _drush_iq_make_description($description) . '-' . $issue_info['id'] . '-' . (1 + count($issue_info['comments']));
+    drush_log(dt("Switching to branch !branchlabel", array('!branchlabel' => $branchlabel)), 'ok');
+    $result = drush_shell_exec_interactive("git checkout -b %s %s", $branchlabel, $branch_merges_with);
+    if (!$result) {
+      return drush_set_error('DRUSH_IQ_BRANCH_CREATE_ERROR', dt("Could not create branch !branch", array('!branch' => $branchlabel)));
+    }
+
+    $result = drush_shell_exec_interactive('git apply -v --directory=%s %s', $project_dir, $filename);
+    if ($result === FALSE) {
+      drush_log(dt("git apply failed; falling back to 'patch' tool"), 'warning');
+    }
+    else {
+      $try_patch_tool = FALSE;
+    }
+  }
+
+  if ($try_patch_tool) {
+    // Try -Np1 first, then -Np0, unless the user selected --no-prefix,
+    // in which case we'll try -Np0 followed by -Np1 if that does not work.
+    $strip_count = 1;
+    if (drush_get_option('no-prefix', FALSE)) {
+      $strip_count = 0;
+    }
+
+    $result = drush_shell_exec_interactive('patch -Np%d --dry-run -d %s -i %s', $strip_count, $project_dir, $filename);
+    if (!$result) {
+      $strip_count = !$strip_count;
+      $result = drush_shell_exec_interactive('patch -Np%d --dry-run -d %s -i %s', $strip_count, $project_dir, $filename);
+      if (!$result) {
+        return drush_set_error('DRUSH_IQ_PATCH_DID_NOT_APPLY', dt("Could not apply the patch with either -Np0 or -Np1"));
+      }
+    }
+    $result = drush_shell_exec_interactive('patch -Np%d -d %s -i %s', $strip_count, $project_dir, $filename);
+  }
+  drush_op('chdir', $cwd);
+  return $result;
+}
+
+/**
+ * This lookup table is used to map items from
+ * the issue queue html to elements in the
+ * $issue_info associative array.  It is also used
+ * to map back again from the $issue_info keys to the
+ * labels in the output of iq-info.
+ */
+function _drush_iq_label_map() {
+  return  array(
+    'Title:' => 'title',
+    'ID:' => 'id',
+    'URL' => 'url',
+    'Project:' => 'projectTitle',
+    'Project URL' => 'projectUrl',
+    'Version:' => 'version',
+    'Component:' => 'component',
+    'Category:' => 'category',
+    'Priority:' => 'priority',
+    'Assigned:' => 'assignedName',
+    'Status:' => 'status',
+  );
+}
+
+/**
+ * Create a commit comment
+ *
+ * @param $issue_info array describing an issue; @see drush_iq_get_info()
+ *
+ * @returns string "#id by contributor1, contributor2: issue title"
+ */
+function _drush_iq_create_commit_comment($issue_info, $committer = FALSE, $message = FALSE) {
+  $contributors = array();
+
+  // If this function is being called because a patch is being
+  // created right now, then add the committer to the head of the
+  // credits list.
+  if ($committer !== FALSE) {
+    // If 'TRUE' is passed for the committer, then look up
+    // the current user name from git config --list
+    if ($committer === TRUE) {
+      drush_shell_exec("git config --list | grep '^user.name=' | sed -e 's|[^=]*=||'");
+      $committer = array_pop(drush_shell_exec_output());
+    }
+
+    if (!empty($committer)) {
+      $contributors[$committer] = $committer;
+    }
+  }
+  // Gather up commit credits, listing most recent contributors first.
+  // Everyone who added an attachment gets credit.
+  if (!empty($issue_info)) {
+    foreach (array_reverse($issue_info['attachments']) as $comment_number => $attachment) {
+      if (array_key_exists($attachment['contributorId'], $issue_info['contributors'])) {
+        $contributor = $issue_info['contributors'][$attachment['contributorId']]['name'];
+        if (!in_array($contributor, $contributors)) {
+          $contributors[$contributor] = $contributor;
+        }
+      }
+    }
+  }
+  $credits = "";
+  $credits_prequel = "";
+  if (!empty($contributors)) {
+    $credits_prequel = " by";
+    $credits = " " . implode(', ', $contributors);
+  }
+
+  $prequel = "";
+  if ($issue_info) {
+    $issue_number = $issue_info['id'];
+    $issue_title = $issue_info['title'];
+    if (!$message) {
+      $message = $issue_title;
+    }
+    $prequel = "Issue #$issue_number$credits_prequel";
+  }
+  if (!$message) {
+    $message = dt("Generated with Drush iq");
+  }
+  return "$prequel$credits: $message";
+}
+
+/**
+ * Get information about an issue
+ *
+ * @param $number integer containing the issue number, or string beginning with a "#" and the issue number
+ *
+ * @returns array
+ *  - title             Title of the issue
+ *  - id                Issue number
+ *  - url               URL to issue page on drupal.org
+ *  - projectTitle      Title of the project issue belongs to (e.g. Drush)
+ *  - projectName       Name of the project (e.g. drush) - http://drupal.org/project/{projectName}
+ *  - projectId
+ *  - projectUrl
+ *  - version           Project version
+ *  - versionId
+ *  - authorName       Name of the user who submitted the issue
+ *  - authorId
+ *  - authorUrl
+ *  - assignedName     Name of the user the issue is assigned to
+ *  - assignedId
+ *  - assignedUrl
+ *  - component         Code, documentation, etc.
+ *  - category          Bug, feature request, etc.
+ *  - priority          minor, normal, major, critical
+ *  - priorityId
+ *  - status            active, needs work, etc.
+ *  - statusId
+ *  - created
+ *  - changed
+ *  - comments          A list (array with numeric keys) of URLs
+ *  - attachments       A list (array with numeric keys) of attachments
+ *      Array
+ *        - contributorId uid of user submitting the patch
+ *        - urls        A list of strings pointing to the attachments
+ *  - contributors      An associative array keyed by uid of contributors
+ *      Array
+ *        - name        Name of contributor
+ *        - uid         uid of contributor (same as key for this item)
+ *        - profile     url to user profile on drupal.org
+ */
+function drush_iq_get_info($issue_spec) {
+  list($number, $comment_number) = drush_iq_issue_number($issue_spec);
+  if (!$number) {
+    return drush_set_error('DRUSH_ISSUE_NOT_FOUND', dt("Could not find the issue !issue", array('!issue' => $issue_spec)));
+  }
+  $issue_info = drush_iq_download_info($number, $comment_number);
+  return $issue_info;
+}
+
+/**
+ *  Given an issue specification,
+ */
+function drush_iq_issue_number($issue_spec) {
+  $number = FALSE;
+  $issue_site_domain = drush_get_option('issue-site', 'drupal.org');
+  $comment_number = FALSE;
+  // #1234
+  if (substr($issue_spec, 0, 1) == '#') {
+    $number = substr($issue_spec, 1);
+  }
+  // http://drupal.org/node/1234
+  elseif (preg_match("#^http://([^/]*)/node/([0-9]*)/*#", $issue_spec, $matches, PREG_OFFSET_CAPTURE)) {
+    $issue_site_domain = $matches[1][0];
+    $number = $matches[2][0];
+  }
+  // 1234
+  elseif (is_numeric($issue_spec)) {
+    $number = $issue_spec;
+  }
+  // description-of-issue-1234 or description-of-issue-1234-8 (description-issue or description-issue-comment)
+  elseif (strpos($issue_spec, ' ') === FALSE) {
+    $issue_spec_parts = explode('-', $issue_spec);
+    $after_last_dash = array_pop($issue_spec_parts);
+    $after_second_to_last_dash = array_pop($issue_spec_parts);
+    if (is_numeric($after_last_dash)) {
+      if (is_numeric($after_second_to_last_dash)) {
+        $comment_number = $after_last_dash;
+        $number = $after_second_to_last_dash;
+        $description = substr($issue_spec, 0, strlen($after_last_dash) + strlen($after_second_to_last_dash));
+      }
+      else {
+        $number = $after_last_dash;
+        $description = substr($issue_spec, 0, strlen($after_last_dash) - 1);
+      }
+    }
+  }
+  return array($number, $comment_number);
+}
+
+/**
+ *  Given a node id, fetch and decode the issue info json from drupal.org.
+ */
+function drush_iq_download_info($number, $comment_number = FALSE) {
+  $issue_site_domain = drush_get_option('issue-site', 'drupal.org');
+  $url = "http://$issue_site_domain/node/$number";
+  // Get the json data from d.o.
+  $filename = drush_find_tmp() . '/project-issue-' . $number . '.json';
+  $filename = _drush_download_file($url . "/project-issue/json", $filename, TRUE);
+  $data = file_get_contents($filename);
+  if (!empty($data)) {
+    $issue_info = json_decode($data, TRUE);
+    // Fixup: if d.o claims that attachment #0 has a contributor, change it to contributor-id
+    if (array_key_exists(0, $issue_info['attachments']) && array_key_exists('contributor', $issue_info['attachments'][0]) && !array_key_exists('contributorId', $issue_info['attachments'][0])) {
+      $issue_info['attachments'][0]['contributorId'] = $issue_info['attachments'][0]['contributor'];
+    }
+    if (!empty($issue_info)) {
+      if ($comment_number) {
+        $issue_info['comment-number'] = $comment_number;
+      }
+      return $issue_info;
+    }
+  }
+  return drush_set_error('DRUSH_PM_ISSUE_FAILED', dt('Could not fetch issue data from !url', array('!url' => $url)));
+}
+
+/*
+ * Pull out information about the contributor of an issue / comment / patch
+ * given a link from the html from the issue page.
+ */
+function _drush_pm_parse_contributor(&$issue_info, $link_xml) {
+  $attributes = $link_xml->attributes();
+  $contributor = array(
+    'name' => (string)$link_xml,
+    'uid' => array_pop(explode('/', (string)$attributes['href'])),
+    'url' => 'http://drupal.org' . (string)$attributes['href'],
+  );
+  $issue_info['contributors'][$contributor['uid']] = $contributor;
+
+  return $contributor;
+}
+
+/*
+ * Pull out information about the attachments from a chunk of
+ * html from the issue page.
+ */
+function _drush_pm_parse_attachments(&$issue_info, $contributor_info, $attachments, $comment_number = 0) {
+  if ($attachments) {
+    $attachment_urls = array();
+    foreach ($attachments[0]->tbody->tr as $tr => $row) {
+      $attachment_attributes = $row->td[0]->a->attributes();
+      $attachment_urls[] = (string)$attachment_attributes['href'];
+    }
+
+    $issue_info['attachments'][$comment_number] = array(
+      'contributor' => $contributor_info['uid'],
+      'urls' => $attachment_urls,
+    );
+  }
+}
+
+/**
+ * Create an ordered list of patches for a given issue.
+ *
+ * @param $issue_info array describing an issue; @see drush_iq_get_info()
+ *
+ * @returns Array list of urls pointing to patch files, ordered oldest to newest.
+ */
+function _drush_iq_find_patches($issue_info) {
+  $patches = array();
+
+  foreach ($issue_info['attachments'] as $comment_number => $attachment) {
+    foreach ($attachment['urls'] as $url) {
+      if (substr($url, -6) == ".patch" || substr($url, -5) == ".diff") {
+        // TODO: detect more than one patch per comment?
+        // Probably cannot handle multiple patches (why would that ever happen?),
+        // but perhaps we could log a warning.
+        $patches[$comment_number] = $url;
+      }
+    }
+  }
+  $index_number = 0;
+  foreach ($issue_info['comments'] as $comment_number => $comment) {
+    ++$index_number;
+    if (array_key_exists($comment_number, $patches)) {
+      $patches[$index_number] = $patches[$comment_number];
+    }
+  }
+  return $patches;
+}
+
+/**
+ * Find the project directory associated with the project
+ * the specified issue is associated with.
+ */
+function _drush_iq_project_dir(&$issue_info) {
+  $project_name = $issue_info['projectName'];
+  $result = FALSE;
+
+  if (array_key_exists('project-dir', $issue_info)) {
+    $result = $issue_info['project-dir'];
+  }
+  else {
+    // If our cwd is at the root of the project, then prefer that project over
+    // one in some other location.
+    $dir = drush_get_context('DRUSH_OLDCWD', drush_cwd());
+    if (basename($dir) == $project_name) {
+      $result = $dir;
+    }
+    // TODO: Find drush extensions such as drush_extras, drush_make, drubuntu, etc.
+    elseif ($project_name == 'drush') {
+      $result = DRUSH_BASE_PATH;
+    }
+    else {
+      $phase = drush_bootstrap_max();
+      drush_log("bootstrapped to phase $phase");
+      if ($phase >= DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION) {
+        $extension_info = drush_get_extensions();
+        // TODO: offer to download the project if it is not found?
+        if (array_key_exists($project_name, $extension_info)) {
+          $result = drush_get_context('DRUSH_DRUPAL_ROOT', '') . '/' . dirname($extension_info[$project_name]->filename);
+        }
+        else {
+          drush_log(dt('Could not find the project directory under the bootstrapped site'));
+        }
+      }
+      elseif ($phase >= DRUSH_BOOTSTRAP_DRUPAL_ROOT) {
+        $root = drush_get_context('DRUSH_DRUPAL_ROOT', FALSE);
+        if ($root) {
+          if ($project_name == 'drupal') {
+            $result = $root;
+          }
+          else {
+            foreach (array('modules', 'sites/all/modules', 'sites/default/modules') as $loc) {
+              $path = $root . '/' . $loc . '/' . $project_name;
+              if (is_dir($path)) {
+                $result = $path;
+              }
+            }
+          }
+        }
+      }
+    }
+    if ($result) {
+      $issue_info['project-dir'] = $result;
+    }
+  }
+
+  return $result;
+}
+
+function _drush_iq_get_branch(&$issue_info) {
+  $branch = FALSE;
+  if (array_key_exists('branch', $issue_info)) {
+    $branch = $issue_info['branch'];
+  }
+  else {
+    $project_dir = _drush_iq_project_dir($issue_info);
+    if ($project_dir) {
+      $branch = _drush_iq_get_branch_at_dir($project_dir);
+    }
+  }
+  $issue_info['branch'] = $branch;
+  return $branch;
+}
+
+function _drush_iq_get_branch_at_dir($dir) {
+  $result = drush_shell_cd_and_exec($dir, "git branch");
+  $branch_output = drush_shell_exec_output();
+
+  // Return the last non-empty line
+  $branch = FALSE;
+  while (($branch === FALSE) && !empty($branch_output)) {
+    $line = array_shift($branch_output);
+    if (!empty($line) && ($line[0] == '*')) {
+      $branch_components = explode(' ', $line, 2);
+      $branch = $branch_components[1];
+    }
+  }
+  return $branch;
+}
+
+function _drush_iq_get_merge_branch_with_remote($merges_with) {
+  $remote = _drush_iq_get_best_remote($merges_with);
+  return _drush_iq_merge_branch_with_remote($remote, $merges_with);
+}
+
+function _drush_iq_merge_branch_with_remote($remote, $merges_with) {
+  if (empty($remote)) {
+    return $merges_with;
+  }
+  else {
+    return $remote . '/' . $merges_with;
+  }
+}
+
+function _drush_iq_get_best_remote($merges_with) {
+  $remotes = _drush_iq_get_remotes($merges_with);
+  if (empty($remotes)) {
+    return "";
+  }
+  elseif (count($remotes) == 1) {
+    return $remotes[0];
+  }
+  elseif (in_array('origin', $remotes)) {
+    return 'origin';
+  }
+  return $remotes[0];
+}
+
+function _drush_iq_get_remotes($merges_with) {
+  // Check the output of `git branch -r`; we are looking
+  // for output "remote/branchname"
+  $result = drush_shell_exec("git branch -r");
+  $git_branch_output = drush_shell_exec_output();
+
+  $remotes = array();
+  foreach($git_branch_output as $line) {
+    $line = trim($line);
+    if (preg_match("#([^/]+)/(.*)#", $line, $matches)) {
+      if ($matches[2] == $merges_with) {
+        $remotes[] = $matches[1];
+      }
+    }
+  }
+  return $remotes;
+}
+
+function _drush_iq_get_branch_merges_with($dir, $branch_label = FALSE) {
+  $merges_with = "";
+  $default_merges_with = FALSE;
+  $alternate_remotes = array();
+
+  if ($branch_label === FALSE) {
+    $branch_label = _drush_iq_get_branch_at_dir($dir);
+  }
+  // We expect that the upstream remote has been configured via
+  // `git branch --set-upstream drush-iq-x master`
+  $result = drush_shell_cd_and_exec($dir, "git config branch.%s.merge", $branch_label);
+  $branch_merge_output = drush_shell_exec_output();
+  if (!empty($branch_merge_output)) {
+    // Convert the branch name into its abbreviated form.
+    $result = drush_shell_exec("git rev-parse --abbrev-ref %s", $branch_merge_output[0]);
+    $rev_parse_output = drush_shell_exec_output();
+    return $rev_parse_output[0];
+  }
+
+  // If the upstream remote has not been configured, then we will look
+  // at the output of `git remote show origin` and see if we can find
+  // a fallback branch to use.
+  $result = drush_shell_cd_and_exec($dir, "git remote show origin -n");
+  $show_orgin_output = drush_shell_exec_output();
+
+  foreach ($show_orgin_output as $line) {
+    $line = trim($line);
+    // Find all of the other branches 'master merges with remote master', etc.
+    if (preg_match("/([^ ]+).* with remote (.*)/", $line, $matches)) {
+      if ($matches[1] == $matches[2]) {
+        $alternate_remotes[] = $matches[2];
+      }
+    }
+  }
+  // If there is only one remote, that must be the one we merge with!
+  if (count($alternate_remotes) == 1) {
+    $merges_with = array_shift($alternate_remotes);
+  }
+  else {
+    // We could figure out which of these branches was the correct
+    // one to use by walking the commit log.  See:
+    // http://drupal.org/node/1078108#comment-6335376
+    $setupstream = "";
+    foreach ($alternate_remotes as $alternate) {
+      $setupstream .= "\n  " . dt("git branch --set-upstream !branch !merge", array('!branch' => $branch_label, '!merge' => $alternate));
+    }
+    return drush_set_error('DRUSH_IQ_CANNOT_FIND_MERGE_BRANCH', dt("Could not determine which branch of !alternatives was the correct merge branch.  Use one of the following commands to select the correct one: !setupstream", array('!alternatives' => implode(',', $alternate_remotes), '!setupstream' => $setupstream)));
+  }
+  return $merges_with;
+}
+
+function _drush_iq_make_description($title) {
+  $description = preg_replace('/[^a-z._-]/', '', str_replace(' ', '-', strtolower($title)));
+
+  // Clip the description off at the next dash after the 30th position
+  if (strlen($description) > 30) {
+    $find_dash = strpos($description, '-', 30);
+    if ($find_dash !== FALSE) {
+      $description = substr($description, 0, $find_dash);
+    }
+  }
+
+  return $description;
+}
diff --git a/docs/iq-commands.html b/docs/iq-commands.html
new file mode 100644
index 0000000..a2a5d4a
--- /dev/null
+++ b/docs/iq-commands.html
@@ -0,0 +1,106 @@
+<h1>Drush Issue Queue Commands</h1>
+<p>
+Drush provides a set of commands to make interacting with the issue queue
+on Drupal.org easier.  In particular, the workflow described by the
+<a href="http://drupal.org/node/1054616">advanced patch contributor guide</a> (<a href="http://drupal.org/node/1054616">http://drupal.org/node/1054616</a>)
+is followed, so the patch files generated will contain information crediting
+the authors.  The Drush issue queue commands makes it much easier for beginners,
+and much faster for beginning and experienced git users alike to create and
+apply patches.
+
+<h3>Applying Patches</h3>
+<p>
+Patches can be applied with the Drush iq-apply-patch command.  The Drush iq
+commands work best with projects that are cloned from their git repository
+on Drupal.org, either by following the instructions at
+http://drupal.org/project/project-name/git-instructions, or by using the
+--package-handler=git_drupalorg modifier with Drush pm-download.
+<pre>
+  $ drush pm-download uuid --package-handler=git_drupalorg
+  $ cd uuid
+  $ drush iq-apply-patch 1236768
+</pre>
+These commands may either be executed from within a working Drupal site,
+or stand-alone, just to see what the patch looks like in context.  If you
+download the module into a working Drupal site, it is not necessary to
+cd to the project directory before running the ip-apply-patch command; Drush
+can find the project directory on its own.
+<p>
+Once you execute these commands, Drush will go out and find the most recent
+patch that has been posted on the given project, and apply it using either
+<pre>git am</pre> or <pre>patch</pre>, as appropriate.  If you do not want to
+apply the most recent patch, then you can append the commit number to the
+end of the issue id; for example, to take the patch from the seventh commit
+of issue 1236768:
+<pre>
+  $drush iq-apply-patch 1236768-7
+</pre>
+Finally, you might sometimes find it convenient to specify a patch via
+the full URL to the issue node, like so:
+<pre>
+  $drush iq-apply-patch http://drupal.org/node/1236768
+</pre>
+This form is handy to use when viewing a project's issue queue; you may
+copy the issue URL to your clipboard by right-clicking on it, and then
+paste it into a terminal window to use wiith iq-apply-patch, all without
+needing to re-open the issue to find the most recent patch.
+<p>
+Before the patch is applied, Drush will create a new branch to store your
+changes in.  This step is optional, and can be subverted by supplying the
+<pre>--no-git</pre> option to iq-apply-patch.  By default, most iq commands do not modify
+the state of your repository; the automatic creation of the Drush iq branch
+is one exception.
+
+<h3>Creating Patches</h3>
+<p>
+After testing and modifying your patch, you may wish to create a new version
+of the patch to post back to the issue queue.  This may be done with the
+Drush iq-diff command.
+<pre>
+  $ drush iq-diff
+</pre>
+The iq-diff command will attempt to use the <pre>git format-patch</code> to
+create the patch, but if you do not desire this, the <pre>--no-git</pre> option
+may be used to create a simple patch instead.  Note that by design, the
+<pre>git format-patch</pre> requires that all modified code be committed
+to the issue branch before it can be included in the patch.  The iq-diff command
+will create a temporary commit to run the format-patch on, but it then
+immediately backs the commit out again, leaving all of your changes in an
+unstaged state.  This is done so that the Drush iq commands can be used
+quickly, without worrying about accumulating unwanted commits in your iq branch.
+If you are a more experienced git user, and prefer to keep these commits around
+to show the history of the patch's development, you may retain them by
+specifying the <pre>--commit</pre> flag when running <pre>drush iq-diff</pre>.
+
+<h3>Committing Changes</h3>
+<p>
+As previously mentioned, it is possible to commit changes to your iq branch
+using the --commit flag with drush iq-diff.  If you would prefer to commit
+your changes via git directly, you may; in this instance, you might want to
+use the iq-create-commit-comment command to facilitate a quick commit.  The
+iq-create-commit-comment command is aliased to ccc, so you can quickly commit
+via:
+<pre>
+  $ git add .
+  $ git commit -m "`drush ccc`"
+</pre>
+
+<h3>Cleaning Up</h3>
+<p>
+When you are done with an iq branch, you may clean it up with an iq-reset
+command.
+<pre>
+  $ drush iq-reset
+</pre>
+This will preserve your iq branch so that you may return to it later.  Note
+that in order for this to work correctly, you must commit your unstaged changes
+to your iq branch first.  If you do not commit, then all of your unstaged
+changes will come with you when you return to the previous main branch that
+the iq branch was split off from.  This is standard git behavior, and the
+iq commands maintain it.
+<p>
+If you are completely done with the iq branch, you may use the <pre>--delete</pre> option
+to perminently delete it.  Since it is easy to re-create the branch with
+iq-apply-patch, it is safe to remove your iq branches after you have posted
+your changes as a new patch on the issue.
+
diff --git a/includes/command.inc b/includes/command.inc
index 04c0a7e..7148d24 100644
--- a/includes/command.inc
+++ b/includes/command.inc
@@ -1087,6 +1087,7 @@ function _drush_command_translate($source) {
  * - callback arguments: an array of arguments to pass to the calback.
  * - description: description of the command.
  * - arguments: an array of arguments that are understood by the command. for help texts.
+ * - required-arguments: The minimum number of arguments that are required, or TRUE if all are required.
  * - options: an array of options that are understood by the command. for help texts.
  * - examples: an array of examples that are understood by the command. for help texts.
  * - scope: one of 'system', 'project', 'site'.
diff --git a/tests/iqTest.php b/tests/iqTest.php
new file mode 100644
index 0000000..6b01dcc
--- /dev/null
+++ b/tests/iqTest.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+  * pm-download testing
+  */
+class iqTestCase extends Drush_CommandTestCase {
+  public function testIssueQueue() {
+    $this->doIssueQueueTests(array());
+  }
+
+  public function testIssueQueueNoGitMode() {
+    $this->setUpFreshSandBox();
+    $this->doIssueQueueTests(array('no-git' => NULL));
+  }
+
+  public function doIssueQueueTests($iq_apply_patch_options) {
+    $options = array(
+      'cache' => NULL,
+      'package-handler' => 'git_drupalorg',
+      'yes' => NULL,
+    );
+    // Download an old version of devel to insulate ourselves from changes
+    $this->drush('pm-download', array('devel-7.x-1.2'), $options);
+    $this->assertFileExists(UNISH_SANDBOX . '/devel/README.txt');
+    $this->assertFileExists(UNISH_SANDBOX . '/devel/.git');
+
+    chdir(UNISH_SANDBOX . '/devel');
+    $this->drush('iq-apply-patch', array('1238344'), $iq_apply_patch_options);
+    $this->drush('iq-diff');
+    $output = $this->getOutput();
+    $this->assertContains("+    \$devel_generate_image_function = variable_get('devel_generate_image_function', '_image_devel_generate');", $output, 'Line added by patch');
+    $this->drush('iq-reset', array(), array('delete' => NULL, 'yes' => NULL));
+    $this->drush('iq-diff');
+    $output = $this->getOutput();
+    //$this->assertEmpty($output, 'Reset removed changes');
+    $this->assertEquals('', $output);
+  }
+}
-- 
1.7.9.5

